dependabot-common 0.95.1 → 0.95.2
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.
- checksums.yaml +4 -4
- data/lib/dependabot.rb +4 -0
- data/lib/dependabot/clients/bitbucket.rb +105 -0
- data/lib/dependabot/clients/github_with_retries.rb +121 -0
- data/lib/dependabot/clients/gitlab.rb +72 -0
- data/lib/dependabot/dependency.rb +115 -0
- data/lib/dependabot/dependency_file.rb +60 -0
- data/lib/dependabot/errors.rb +179 -0
- data/lib/dependabot/file_fetchers.rb +18 -0
- data/lib/dependabot/file_fetchers/README.md +65 -0
- data/lib/dependabot/file_fetchers/base.rb +368 -0
- data/lib/dependabot/file_parsers.rb +18 -0
- data/lib/dependabot/file_parsers/README.md +45 -0
- data/lib/dependabot/file_parsers/base.rb +31 -0
- data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
- data/lib/dependabot/file_updaters.rb +18 -0
- data/lib/dependabot/file_updaters/README.md +58 -0
- data/lib/dependabot/file_updaters/base.rb +52 -0
- data/lib/dependabot/git_commit_checker.rb +412 -0
- data/lib/dependabot/metadata_finders.rb +18 -0
- data/lib/dependabot/metadata_finders/README.md +53 -0
- data/lib/dependabot/metadata_finders/base.rb +117 -0
- data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
- data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
- data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
- data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
- data/lib/dependabot/pull_request_creator.rb +155 -0
- data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
- data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
- data/lib/dependabot/pull_request_creator/github.rb +277 -0
- data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
- data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
- data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
- data/lib/dependabot/pull_request_updater.rb +43 -0
- data/lib/dependabot/pull_request_updater/github.rb +165 -0
- data/lib/dependabot/shared_helpers.rb +224 -0
- data/lib/dependabot/source.rb +120 -0
- data/lib/dependabot/update_checkers.rb +18 -0
- data/lib/dependabot/update_checkers/README.md +67 -0
- data/lib/dependabot/update_checkers/base.rb +220 -0
- data/lib/dependabot/utils.rb +33 -0
- data/lib/dependabot/version.rb +5 -0
- data/lib/rubygems_version_patch.rb +14 -0
- metadata +44 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f7232935dc073586ef8ecbb1ef3adfea19541485f74211cefd33c9025d30d01
|
|
4
|
+
data.tar.gz: e1516d883396351cdcc282a93f6b11c30c94128d9b176b698c46067772ca5fc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b529b1074403b82ba80cfb22f99b3ac3e1231a99b96b54b86eb49974cf95e7ee95d3cf12e1d9d2bc6d7536713ec8510a8cb7c29063a0263f73d67687f0e7e9e
|
|
7
|
+
data.tar.gz: a0de54b828ee9958e0d6906a1cd09603f908460303dfb5eec3f55be4af48d44557edd80b666e40bde523687056f1e09a70dadef7bdd9edde354920646f1daacb
|
data/lib/dependabot.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/shared_helpers"
|
|
4
|
+
require "excon"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Clients
|
|
8
|
+
class Bitbucket
|
|
9
|
+
class NotFound < StandardError; end
|
|
10
|
+
class Unauthorized < StandardError; end
|
|
11
|
+
class Forbidden < StandardError; end
|
|
12
|
+
|
|
13
|
+
#######################
|
|
14
|
+
# Constructor methods #
|
|
15
|
+
#######################
|
|
16
|
+
|
|
17
|
+
def self.for_bitbucket_dot_org(credentials:)
|
|
18
|
+
credential =
|
|
19
|
+
credentials.
|
|
20
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
21
|
+
find { |cred| cred["host"] == "bitbucket.org" }
|
|
22
|
+
|
|
23
|
+
new(credential)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##########
|
|
27
|
+
# Client #
|
|
28
|
+
##########
|
|
29
|
+
|
|
30
|
+
def initialize(credentials)
|
|
31
|
+
@credentials = credentials
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fetch_commit(repo, branch)
|
|
35
|
+
path = "#{repo}/refs/branches/#{branch}"
|
|
36
|
+
response = get(base_url + path)
|
|
37
|
+
|
|
38
|
+
JSON.parse(response.body).fetch("target").fetch("hash")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fetch_default_branch(repo)
|
|
42
|
+
response = get(base_url + repo)
|
|
43
|
+
|
|
44
|
+
JSON.parse(response.body).fetch("mainbranch").fetch("name")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fetch_repo_contents(repo, commit = nil, path = nil)
|
|
48
|
+
raise "Commit is required if path provided!" if commit.nil? && path
|
|
49
|
+
|
|
50
|
+
api_path = "#{repo}/src"
|
|
51
|
+
api_path += "/#{commit}" if commit
|
|
52
|
+
api_path += "/#{path.gsub(%r{/+$}, '')}" if path
|
|
53
|
+
api_path += "?pagelen=100"
|
|
54
|
+
response = get(base_url + api_path)
|
|
55
|
+
|
|
56
|
+
JSON.parse(response.body).fetch("values")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fetch_file_contents(repo, commit, path)
|
|
60
|
+
path = "#{repo}/src/#{commit}/#{path.gsub(%r{/+$}, '')}"
|
|
61
|
+
response = get(base_url + path)
|
|
62
|
+
|
|
63
|
+
response.body
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tags(repo)
|
|
67
|
+
path = "#{repo}/refs/tags?pagelen=100"
|
|
68
|
+
response = get(base_url + path)
|
|
69
|
+
|
|
70
|
+
JSON.parse(response.body).fetch("values")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def compare(repo, previous_tag, new_tag)
|
|
74
|
+
path = "#{repo}/commits/?include=#{new_tag}&exclude=#{previous_tag}"
|
|
75
|
+
response = get(base_url + path)
|
|
76
|
+
|
|
77
|
+
JSON.parse(response.body).fetch("values")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def get(url)
|
|
81
|
+
response = Excon.get(
|
|
82
|
+
url,
|
|
83
|
+
user: credentials&.fetch("username"),
|
|
84
|
+
password: credentials&.fetch("password"),
|
|
85
|
+
idempotent: true,
|
|
86
|
+
**Dependabot::SharedHelpers.excon_defaults
|
|
87
|
+
)
|
|
88
|
+
raise Unauthorized if response.status == 401
|
|
89
|
+
raise Forbidden if response.status == 403
|
|
90
|
+
raise NotFound if response.status == 404
|
|
91
|
+
|
|
92
|
+
response
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
attr_reader :credentials
|
|
98
|
+
|
|
99
|
+
def base_url
|
|
100
|
+
# TODO: Make this configurable when we support enterprise Bitbucket
|
|
101
|
+
"https://api.bitbucket.org/2.0/repositories/"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "octokit"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
module Clients
|
|
7
|
+
class GithubWithRetries
|
|
8
|
+
DEFAULT_CLIENT_ARGS = {
|
|
9
|
+
connection_options: {
|
|
10
|
+
request: {
|
|
11
|
+
open_timeout: 2,
|
|
12
|
+
timeout: 5
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
RETRYABLE_ERRORS = [
|
|
18
|
+
Faraday::ConnectionFailed,
|
|
19
|
+
Faraday::TimeoutError,
|
|
20
|
+
Octokit::InternalServerError,
|
|
21
|
+
Octokit::BadGateway
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
#######################
|
|
25
|
+
# Constructor methods #
|
|
26
|
+
#######################
|
|
27
|
+
|
|
28
|
+
def self.for_source(source:, credentials:)
|
|
29
|
+
access_tokens =
|
|
30
|
+
credentials.
|
|
31
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
32
|
+
select { |cred| cred["host"] == source.hostname }.
|
|
33
|
+
map { |cred| cred.fetch("password") }
|
|
34
|
+
|
|
35
|
+
new(
|
|
36
|
+
access_tokens: access_tokens,
|
|
37
|
+
api_endpoint: source.api_endpoint
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.for_github_dot_com(credentials:)
|
|
42
|
+
access_tokens =
|
|
43
|
+
credentials.
|
|
44
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
45
|
+
select { |cred| cred["host"] == "github.com" }.
|
|
46
|
+
map { |cred| cred.fetch("password") }
|
|
47
|
+
|
|
48
|
+
new(access_tokens: access_tokens)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#################
|
|
52
|
+
# VCS Interface #
|
|
53
|
+
#################
|
|
54
|
+
|
|
55
|
+
def fetch_commit(repo, branch)
|
|
56
|
+
response = ref(repo, "heads/#{branch}")
|
|
57
|
+
|
|
58
|
+
raise Octokit::NotFound if response.is_a?(Array)
|
|
59
|
+
|
|
60
|
+
response.object.sha
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_default_branch(repo)
|
|
64
|
+
repository(repo).default_branch
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
############
|
|
68
|
+
# Proxying #
|
|
69
|
+
############
|
|
70
|
+
|
|
71
|
+
def initialize(max_retries: 3, **args)
|
|
72
|
+
args = DEFAULT_CLIENT_ARGS.merge(args)
|
|
73
|
+
|
|
74
|
+
access_tokens = args.delete(:access_tokens) || []
|
|
75
|
+
access_tokens << args[:access_token] if args[:access_token]
|
|
76
|
+
access_tokens << nil if access_tokens.empty?
|
|
77
|
+
access_tokens.uniq!
|
|
78
|
+
|
|
79
|
+
@max_retries = max_retries || 3
|
|
80
|
+
@clients = access_tokens.map do |token|
|
|
81
|
+
Octokit::Client.new(args.merge(access_token: token))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def method_missing(method_name, *args, &block)
|
|
86
|
+
untried_clients = @clients.dup
|
|
87
|
+
client = untried_clients.pop
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
retry_connection_failures do
|
|
91
|
+
if client.respond_to?(method_name)
|
|
92
|
+
mutatable_args = args.map(&:dup)
|
|
93
|
+
client.public_send(method_name, *mutatable_args, &block)
|
|
94
|
+
else
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
rescue Octokit::NotFound, Octokit::Unauthorized, Octokit::Forbidden
|
|
99
|
+
raise unless (client = untried_clients.pop)
|
|
100
|
+
|
|
101
|
+
retry
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
106
|
+
@clients.first.respond_to?(method_name) || super
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def retry_connection_failures
|
|
110
|
+
retry_attempt = 0
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
yield
|
|
114
|
+
rescue *RETRYABLE_ERRORS
|
|
115
|
+
retry_attempt += 1
|
|
116
|
+
retry_attempt <= @max_retries ? retry : raise
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gitlab"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
module Clients
|
|
7
|
+
class Gitlab
|
|
8
|
+
#######################
|
|
9
|
+
# Constructor methods #
|
|
10
|
+
#######################
|
|
11
|
+
|
|
12
|
+
def self.for_source(source:, credentials:)
|
|
13
|
+
access_token =
|
|
14
|
+
credentials.
|
|
15
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
16
|
+
find { |cred| cred["host"] == source.hostname }&.
|
|
17
|
+
fetch("password")
|
|
18
|
+
|
|
19
|
+
new(
|
|
20
|
+
endpoint: source.api_endpoint,
|
|
21
|
+
private_token: access_token || ""
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.for_gitlab_dot_com(credentials:)
|
|
26
|
+
access_token =
|
|
27
|
+
credentials.
|
|
28
|
+
select { |cred| cred["type"] == "git_source" }.
|
|
29
|
+
find { |cred| cred["host"] == "gitlab.com" }&.
|
|
30
|
+
fetch("password")
|
|
31
|
+
|
|
32
|
+
new(
|
|
33
|
+
endpoint: "https://gitlab.com/api/v4",
|
|
34
|
+
private_token: access_token || ""
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#################
|
|
39
|
+
# VCS Interface #
|
|
40
|
+
#################
|
|
41
|
+
|
|
42
|
+
def fetch_commit(repo, branch)
|
|
43
|
+
branch(repo, branch).commit.id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch_default_branch(repo)
|
|
47
|
+
project(repo).default_branch
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
############
|
|
51
|
+
# Proxying #
|
|
52
|
+
############
|
|
53
|
+
|
|
54
|
+
def initialize(**args)
|
|
55
|
+
@client = ::Gitlab::Client.new(args)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def method_missing(method_name, *args, &block)
|
|
59
|
+
if @client.respond_to?(method_name)
|
|
60
|
+
mutatable_args = args.map(&:dup)
|
|
61
|
+
@client.public_send(method_name, *mutatable_args, &block)
|
|
62
|
+
else
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
68
|
+
@client.respond_to?(method_name) || super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems_version_patch"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
class Dependency
|
|
7
|
+
@production_checks = {}
|
|
8
|
+
|
|
9
|
+
def self.production_check_for_package_manager(package_manager)
|
|
10
|
+
production_check = @production_checks[package_manager]
|
|
11
|
+
return production_check if production_check
|
|
12
|
+
|
|
13
|
+
raise "Unsupported package_manager #{package_manager}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.register_production_check(package_manager, production_check)
|
|
17
|
+
@production_checks[package_manager] = production_check
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :name, :version, :requirements, :package_manager,
|
|
21
|
+
:previous_version, :previous_requirements
|
|
22
|
+
|
|
23
|
+
def initialize(name:, requirements:, package_manager:, version: nil,
|
|
24
|
+
previous_version: nil, previous_requirements: nil)
|
|
25
|
+
@name = name
|
|
26
|
+
@version = version
|
|
27
|
+
@requirements = requirements.map { |req| symbolize_keys(req) }
|
|
28
|
+
@previous_version = previous_version
|
|
29
|
+
@previous_requirements =
|
|
30
|
+
previous_requirements&.map { |req| symbolize_keys(req) }
|
|
31
|
+
@package_manager = package_manager
|
|
32
|
+
|
|
33
|
+
check_values
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def top_level?
|
|
37
|
+
requirements.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
"name" => name,
|
|
43
|
+
"version" => version,
|
|
44
|
+
"requirements" => requirements,
|
|
45
|
+
"previous_version" => previous_version,
|
|
46
|
+
"previous_requirements" => previous_requirements,
|
|
47
|
+
"package_manager" => package_manager
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def appears_in_lockfile?
|
|
52
|
+
previous_version || (version && previous_requirements.nil?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def production?
|
|
56
|
+
return true unless top_level?
|
|
57
|
+
|
|
58
|
+
groups = requirements.flat_map { |r| r.fetch(:groups).map(&:to_s) }
|
|
59
|
+
|
|
60
|
+
self.class.
|
|
61
|
+
production_check_for_package_manager(package_manager).
|
|
62
|
+
call(groups)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def display_name
|
|
66
|
+
return name unless %w(maven gradle).include?(package_manager)
|
|
67
|
+
|
|
68
|
+
name.split(":").last
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.instance_of?(self.class) && to_h == other.to_h
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def hash
|
|
76
|
+
to_h.hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def eql?(other)
|
|
80
|
+
self.==(other)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def check_values
|
|
86
|
+
if [version, previous_version].any? { |v| v == "" }
|
|
87
|
+
raise ArgumentError, "blank strings must not be provided as versions"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
requirement_fields = [requirements, previous_requirements].compact
|
|
91
|
+
unless requirement_fields.all? { |r| r.is_a?(Array) } &&
|
|
92
|
+
requirement_fields.flatten.all? { |r| r.is_a?(Hash) }
|
|
93
|
+
raise ArgumentError, "requirements must be an array of hashes"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
required_keys = %i(requirement file groups source)
|
|
97
|
+
optional_keys = %i(metadata)
|
|
98
|
+
unless requirement_fields.flatten.
|
|
99
|
+
all? { |r| required_keys.sort == (r.keys - optional_keys).sort }
|
|
100
|
+
raise ArgumentError, "each requirement must have the following "\
|
|
101
|
+
"required keys: #{required_keys.join(', ')}."\
|
|
102
|
+
"Optionally, it may have the following keys: "\
|
|
103
|
+
"#{optional_keys.join(', ')}."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return if requirement_fields.flatten.none? { |r| r[:requirement] == "" }
|
|
107
|
+
|
|
108
|
+
raise ArgumentError, "blank strings must not be provided as requirements"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def symbolize_keys(hash)
|
|
112
|
+
Hash[hash.keys.map { |k| [k.to_sym, hash[k]] }]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
class DependencyFile
|
|
7
|
+
attr_accessor :name, :content, :directory, :type, :support_file
|
|
8
|
+
|
|
9
|
+
def initialize(name:, content:, directory: "/", type: "file",
|
|
10
|
+
support_file: false)
|
|
11
|
+
@name = name
|
|
12
|
+
@content = content
|
|
13
|
+
@directory = clean_directory(directory)
|
|
14
|
+
@support_file = support_file
|
|
15
|
+
|
|
16
|
+
# Type is used *very* sparingly. It lets the git_modules updater know that
|
|
17
|
+
# a "file" is actually a submodule, and lets our Go updaters know which
|
|
18
|
+
# file represents the main.go.
|
|
19
|
+
# New use cases should be avoided if at all possible (and use the
|
|
20
|
+
# support_file flag instead)
|
|
21
|
+
@type = type
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"name" => name,
|
|
27
|
+
"content" => content,
|
|
28
|
+
"directory" => directory,
|
|
29
|
+
"type" => type
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def path
|
|
34
|
+
Pathname.new(File.join(directory, name)).cleanpath.to_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ==(other)
|
|
38
|
+
other.instance_of?(self.class) && to_h == other.to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hash
|
|
42
|
+
to_h.hash
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def eql?(other)
|
|
46
|
+
self.==(other)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def support_file?
|
|
50
|
+
@support_file
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def clean_directory(directory)
|
|
56
|
+
# Directory should always start with a `/`
|
|
57
|
+
directory.sub(%r{^/*}, "/")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|