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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/pull_request_updater/github"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
class PullRequestUpdater
|
|
7
|
+
attr_reader :source, :files, :base_commit, :credentials,
|
|
8
|
+
:pull_request_number, :author_details, :signature_key
|
|
9
|
+
|
|
10
|
+
def initialize(source:, base_commit:, files:, credentials:,
|
|
11
|
+
pull_request_number:, author_details: nil,
|
|
12
|
+
signature_key: nil)
|
|
13
|
+
@source = source
|
|
14
|
+
@base_commit = base_commit
|
|
15
|
+
@files = files
|
|
16
|
+
@credentials = credentials
|
|
17
|
+
@pull_request_number = pull_request_number
|
|
18
|
+
@author_details = author_details
|
|
19
|
+
@signature_key = signature_key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update
|
|
23
|
+
case source.provider
|
|
24
|
+
when "github" then github_updater.update
|
|
25
|
+
else raise "Unsupported provider #{source.provider}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def github_updater
|
|
32
|
+
Github.new(
|
|
33
|
+
source: source,
|
|
34
|
+
base_commit: base_commit,
|
|
35
|
+
files: files,
|
|
36
|
+
credentials: credentials,
|
|
37
|
+
pull_request_number: pull_request_number,
|
|
38
|
+
author_details: author_details,
|
|
39
|
+
signature_key: signature_key
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "octokit"
|
|
4
|
+
require "dependabot/clients/github_with_retries"
|
|
5
|
+
require "dependabot/pull_request_creator/commit_signer"
|
|
6
|
+
require "dependabot/pull_request_updater"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
class PullRequestUpdater
|
|
10
|
+
class Github
|
|
11
|
+
attr_reader :source, :files, :base_commit, :credentials,
|
|
12
|
+
:pull_request_number, :author_details, :signature_key
|
|
13
|
+
|
|
14
|
+
def initialize(source:, base_commit:, files:, credentials:,
|
|
15
|
+
pull_request_number:, author_details: nil,
|
|
16
|
+
signature_key: nil)
|
|
17
|
+
@source = source
|
|
18
|
+
@base_commit = base_commit
|
|
19
|
+
@files = files
|
|
20
|
+
@credentials = credentials
|
|
21
|
+
@pull_request_number = pull_request_number
|
|
22
|
+
@author_details = author_details
|
|
23
|
+
@signature_key = signature_key
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
return unless branch_exists?
|
|
28
|
+
|
|
29
|
+
commit = create_commit
|
|
30
|
+
branch = update_branch(commit)
|
|
31
|
+
update_pull_request_target_branch
|
|
32
|
+
branch
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def update_pull_request_target_branch
|
|
38
|
+
target_branch = source.branch || pull_request.base.repo.default_branch
|
|
39
|
+
return if target_branch == pull_request.base.ref
|
|
40
|
+
|
|
41
|
+
github_client_for_source.update_pull_request(
|
|
42
|
+
source.repo,
|
|
43
|
+
pull_request_number,
|
|
44
|
+
base: target_branch
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def github_client_for_source
|
|
49
|
+
@github_client_for_source ||=
|
|
50
|
+
Dependabot::Clients::GithubWithRetries.for_source(
|
|
51
|
+
source: source,
|
|
52
|
+
credentials: credentials
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pull_request
|
|
57
|
+
@pull_request ||=
|
|
58
|
+
github_client_for_source.pull_request(
|
|
59
|
+
source.repo,
|
|
60
|
+
pull_request_number
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def branch_exists?
|
|
65
|
+
github_client_for_source.branch(source.repo, pull_request.head.ref)
|
|
66
|
+
rescue Octokit::NotFound
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_commit
|
|
71
|
+
tree = create_tree
|
|
72
|
+
|
|
73
|
+
options = author_details&.any? ? { author: author_details } : {}
|
|
74
|
+
|
|
75
|
+
if options[:author]&.any? && signature_key
|
|
76
|
+
options[:author][:date] = Time.now.utc.iso8601
|
|
77
|
+
options[:signature] = commit_signature(tree, options[:author])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
github_client_for_source.create_commit(
|
|
81
|
+
source.repo,
|
|
82
|
+
commit_message,
|
|
83
|
+
tree.sha,
|
|
84
|
+
base_commit,
|
|
85
|
+
options
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def create_tree
|
|
90
|
+
file_trees = files.map do |file|
|
|
91
|
+
if file.type == "file"
|
|
92
|
+
{
|
|
93
|
+
path: file.path.sub(%r{^/}, ""),
|
|
94
|
+
mode: "100644",
|
|
95
|
+
type: "blob",
|
|
96
|
+
content: file.content
|
|
97
|
+
}
|
|
98
|
+
elsif file.type == "submodule"
|
|
99
|
+
{
|
|
100
|
+
path: file.path.sub(%r{^/}, ""),
|
|
101
|
+
mode: "160000",
|
|
102
|
+
type: "commit",
|
|
103
|
+
sha: file.content
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
raise "Unknown file type #{file.type}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
github_client_for_source.create_tree(
|
|
111
|
+
source.repo,
|
|
112
|
+
file_trees,
|
|
113
|
+
base_tree: base_commit
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def update_branch(commit)
|
|
118
|
+
github_client_for_source.update_ref(
|
|
119
|
+
source.repo,
|
|
120
|
+
"heads/" + pull_request.head.ref,
|
|
121
|
+
commit.sha,
|
|
122
|
+
true
|
|
123
|
+
)
|
|
124
|
+
rescue Octokit::UnprocessableEntity => error
|
|
125
|
+
# Return quietly if the branch has been deleted
|
|
126
|
+
return nil if error.message.match?(/Reference does not exist/i)
|
|
127
|
+
|
|
128
|
+
# Return quietly if the branch has been merged
|
|
129
|
+
return nil if error.message.match?(/Reference cannot be updated/i)
|
|
130
|
+
|
|
131
|
+
raise
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def commit_message
|
|
135
|
+
@commit_message ||=
|
|
136
|
+
if pull_request.commits == 1
|
|
137
|
+
github_client_for_source.
|
|
138
|
+
git_commit(source.repo, pull_request.head.sha).
|
|
139
|
+
message
|
|
140
|
+
else
|
|
141
|
+
author_name = author_details&.fetch(:name, nil) || "dependabot[bot]"
|
|
142
|
+
commits =
|
|
143
|
+
github_client_for_source.
|
|
144
|
+
pull_request_commits(source.repo, pull_request_number)
|
|
145
|
+
|
|
146
|
+
commit =
|
|
147
|
+
commits.find { |c| c.commit.author.name == author_name } ||
|
|
148
|
+
commits.first
|
|
149
|
+
|
|
150
|
+
commit.commit.message
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def commit_signature(tree, author_details_with_date)
|
|
155
|
+
PullRequestCreator::CommitSigner.new(
|
|
156
|
+
author_details: author_details_with_date,
|
|
157
|
+
commit_message: commit_message,
|
|
158
|
+
tree_sha: tree.sha,
|
|
159
|
+
parent_sha: base_commit,
|
|
160
|
+
signature_key: signature_key
|
|
161
|
+
).signature
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "excon"
|
|
6
|
+
require "English"
|
|
7
|
+
require "digest"
|
|
8
|
+
require "open3"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module SharedHelpers
|
|
12
|
+
BUMP_TMP_FILE_PREFIX = "dependabot_"
|
|
13
|
+
BUMP_TMP_DIR_PATH = "tmp"
|
|
14
|
+
GIT_CONFIG_GLOBAL_PATH = File.expand_path("~/.gitconfig")
|
|
15
|
+
|
|
16
|
+
class ChildProcessFailed < StandardError
|
|
17
|
+
attr_reader :error_class, :error_message, :error_backtrace
|
|
18
|
+
|
|
19
|
+
def initialize(error_class:, error_message:, error_backtrace:)
|
|
20
|
+
@error_class = error_class
|
|
21
|
+
@error_message = error_message
|
|
22
|
+
@error_backtrace = error_backtrace
|
|
23
|
+
|
|
24
|
+
msg = "Child process raised #{error_class} with message: "\
|
|
25
|
+
"#{error_message}"
|
|
26
|
+
super(msg)
|
|
27
|
+
set_backtrace(error_backtrace)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.in_a_temporary_directory(directory = "/")
|
|
32
|
+
Dir.mkdir(BUMP_TMP_DIR_PATH) unless Dir.exist?(BUMP_TMP_DIR_PATH)
|
|
33
|
+
Dir.mktmpdir(BUMP_TMP_FILE_PREFIX, BUMP_TMP_DIR_PATH) do |dir|
|
|
34
|
+
path = Pathname.new(File.join(dir, directory)).expand_path
|
|
35
|
+
FileUtils.mkpath(path)
|
|
36
|
+
Dir.chdir(path) { yield(path) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.in_a_forked_process
|
|
41
|
+
read, write = IO.pipe
|
|
42
|
+
|
|
43
|
+
pid = fork do
|
|
44
|
+
read.close
|
|
45
|
+
result = yield
|
|
46
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
47
|
+
result = { _error_details: { error_class: error.class.to_s,
|
|
48
|
+
error_message: error.message,
|
|
49
|
+
error_backtrace: error.backtrace } }
|
|
50
|
+
ensure
|
|
51
|
+
Marshal.dump(result, write)
|
|
52
|
+
exit!(0)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
write.close
|
|
56
|
+
result = read.read
|
|
57
|
+
Process.wait(pid)
|
|
58
|
+
result = Marshal.load(result) # rubocop:disable Security/MarshalLoad
|
|
59
|
+
|
|
60
|
+
return result unless result.is_a?(Hash) && result[:_error_details]
|
|
61
|
+
|
|
62
|
+
raise ChildProcessFailed, result[:_error_details]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class HelperSubprocessFailed < StandardError
|
|
66
|
+
def initialize(message:, error_context:)
|
|
67
|
+
super(message)
|
|
68
|
+
@error_context = error_context
|
|
69
|
+
@command = error_context[:command]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def raven_context
|
|
73
|
+
{ fingerprint: [@command], extra: @error_context }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.run_helper_subprocess(command:, function:, args:, env: nil,
|
|
78
|
+
stderr_to_stdout: false)
|
|
79
|
+
start = Time.now
|
|
80
|
+
stdin_data = JSON.dump(function: function, args: args)
|
|
81
|
+
env_cmd = [env, command].compact
|
|
82
|
+
stdout, stderr, process = Open3.capture3(*env_cmd, stdin_data: stdin_data)
|
|
83
|
+
time_taken = Time.now - start
|
|
84
|
+
|
|
85
|
+
# Some package managers output useful stuff to stderr instead of stdout so
|
|
86
|
+
# we want to parse this, most package manager will output garbage here so
|
|
87
|
+
# would mess up json response from stdout
|
|
88
|
+
stdout = "#{stderr}\n#{stdout}" if stderr_to_stdout
|
|
89
|
+
|
|
90
|
+
error_context = {
|
|
91
|
+
command: command,
|
|
92
|
+
function: function,
|
|
93
|
+
args: args,
|
|
94
|
+
time_taken: time_taken,
|
|
95
|
+
stderr_output: stderr ? stderr[0..50_000] : "", # Truncate to ~100kb
|
|
96
|
+
process_exit_value: process.to_s
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
response = JSON.parse(stdout)
|
|
100
|
+
return response["result"] if process.success?
|
|
101
|
+
|
|
102
|
+
raise HelperSubprocessFailed.new(
|
|
103
|
+
message: response["error"],
|
|
104
|
+
error_context: error_context
|
|
105
|
+
)
|
|
106
|
+
rescue JSON::ParserError
|
|
107
|
+
raise HelperSubprocessFailed.new(
|
|
108
|
+
message: stdout || "No output from command",
|
|
109
|
+
error_context: error_context
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.excon_middleware
|
|
114
|
+
Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.excon_defaults
|
|
118
|
+
{
|
|
119
|
+
connect_timeout: 5,
|
|
120
|
+
write_timeout: 5,
|
|
121
|
+
read_timeout: 5,
|
|
122
|
+
omit_default_port: true,
|
|
123
|
+
middlewares: excon_middleware
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.with_git_configured(credentials:)
|
|
128
|
+
backup_git_config_path = stash_global_git_config
|
|
129
|
+
configure_git_to_use_https_with_credentials(credentials)
|
|
130
|
+
yield
|
|
131
|
+
ensure
|
|
132
|
+
reset_global_git_config(backup_git_config_path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.configure_git_to_use_https_with_credentials(credentials)
|
|
136
|
+
configure_git_to_use_https
|
|
137
|
+
configure_git_credentials(credentials)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.configure_git_to_use_https
|
|
141
|
+
# Note: we use --global here (rather than --system) so that Dependabot
|
|
142
|
+
# can be run without privileged access
|
|
143
|
+
run_shell_command(
|
|
144
|
+
'git config --global --replace-all url."https://github.com/".'\
|
|
145
|
+
"insteadOf ssh://git@github.com/ && "\
|
|
146
|
+
'git config --global --add url."https://github.com/".'\
|
|
147
|
+
"insteadOf ssh://git@github.com: && "\
|
|
148
|
+
'git config --global --add url."https://github.com/".'\
|
|
149
|
+
"insteadOf git@github.com: && "\
|
|
150
|
+
'git config --global --add url."https://github.com/".'\
|
|
151
|
+
"insteadOf git@github.com/ && "\
|
|
152
|
+
'git config --global --add url."https://github.com/".'\
|
|
153
|
+
"insteadOf git://github.com/"
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.configure_git_credentials(credentials)
|
|
158
|
+
# Then add a file-based credential store that loads a file in this repo.
|
|
159
|
+
# Under the hood this uses git credential-store, but it's invoked through
|
|
160
|
+
# an wrapper binary that only allows non-mutative commands. Without this,
|
|
161
|
+
# whenever the credentials are deemed to be invalid, they're erased.
|
|
162
|
+
credential_helper_path =
|
|
163
|
+
File.join(__dir__, "../../bin/git-credential-store-immutable")
|
|
164
|
+
run_shell_command(
|
|
165
|
+
"git config --global credential.helper "\
|
|
166
|
+
"'#{credential_helper_path} --file=#{Dir.pwd}/git.store'"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Build the content for our credentials file
|
|
170
|
+
git_store_content = ""
|
|
171
|
+
credentials.each do |cred|
|
|
172
|
+
next unless cred["type"] == "git_source"
|
|
173
|
+
|
|
174
|
+
authenticated_url =
|
|
175
|
+
"https://#{cred.fetch('username')}:#{cred.fetch('password')}"\
|
|
176
|
+
"@#{cred.fetch('host')}"
|
|
177
|
+
|
|
178
|
+
git_store_content += authenticated_url + "\n"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Save the file
|
|
182
|
+
File.write("git.store", git_store_content)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.stash_global_git_config
|
|
186
|
+
return unless File.exist?(GIT_CONFIG_GLOBAL_PATH)
|
|
187
|
+
|
|
188
|
+
contents = File.read(GIT_CONFIG_GLOBAL_PATH)
|
|
189
|
+
digest = Digest::SHA2.hexdigest(contents)[0...10]
|
|
190
|
+
backup_path = GIT_CONFIG_GLOBAL_PATH + ".backup-#{digest}"
|
|
191
|
+
|
|
192
|
+
FileUtils.mv(GIT_CONFIG_GLOBAL_PATH, backup_path)
|
|
193
|
+
backup_path
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.reset_global_git_config(backup_path)
|
|
197
|
+
return if backup_path.nil?
|
|
198
|
+
return unless File.exist?(backup_path)
|
|
199
|
+
|
|
200
|
+
FileUtils.mv(backup_path, GIT_CONFIG_GLOBAL_PATH)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.run_shell_command(command)
|
|
204
|
+
start = Time.now
|
|
205
|
+
stdout, process = Open3.capture2e(command)
|
|
206
|
+
time_taken = start - Time.now
|
|
207
|
+
|
|
208
|
+
# Raise an error with the output from the shell session if the
|
|
209
|
+
# command returns a non-zero status
|
|
210
|
+
return if process.success?
|
|
211
|
+
|
|
212
|
+
error_context = {
|
|
213
|
+
command: command,
|
|
214
|
+
time_taken: time_taken,
|
|
215
|
+
process_exit_value: process.to_s
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
raise SharedHelpers::HelperSubprocessFailed.new(
|
|
219
|
+
message: stdout,
|
|
220
|
+
error_context: error_context
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dependabot
|
|
4
|
+
class Source
|
|
5
|
+
GITHUB_SOURCE = %r{
|
|
6
|
+
(?<provider>github)
|
|
7
|
+
(?:\.com)[/:]
|
|
8
|
+
(?<repo>[\w.-]+/(?:(?!\.git|\.\s)[\w.-])+)
|
|
9
|
+
(?:(?:/tree|/blob)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
|
|
10
|
+
}x.freeze
|
|
11
|
+
|
|
12
|
+
GITLAB_SOURCE = %r{
|
|
13
|
+
(?<provider>gitlab)
|
|
14
|
+
(?:\.com)[/:]
|
|
15
|
+
(?<repo>[^/\s]+/(?:(?!\.git|\.\s)[^/\s#"',])+)
|
|
16
|
+
(?:(?:/tree|/blob)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
|
|
17
|
+
}x.freeze
|
|
18
|
+
|
|
19
|
+
BITBUCKET_SOURCE = %r{
|
|
20
|
+
(?<provider>bitbucket)
|
|
21
|
+
(?:\.org)[/:]
|
|
22
|
+
(?<repo>[^/\s]+/(?:(?!\.git|\.\s)[^/\s#"',])+)
|
|
23
|
+
(?:(?:/src)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
|
|
24
|
+
}x.freeze
|
|
25
|
+
|
|
26
|
+
AZURE_SOURCE = %r{
|
|
27
|
+
(?<provider>azure)
|
|
28
|
+
(?:\.com)[/:]
|
|
29
|
+
(?<repo>[^/\s]+/([^/\s]+/)?(?:_git/)(?:(?!\.git|\.\s)[^/\s#?"',])+)
|
|
30
|
+
}x.freeze
|
|
31
|
+
|
|
32
|
+
SOURCE_REGEX = /
|
|
33
|
+
(?:#{GITHUB_SOURCE})|
|
|
34
|
+
(?:#{GITLAB_SOURCE})|
|
|
35
|
+
(?:#{BITBUCKET_SOURCE})|
|
|
36
|
+
(?:#{AZURE_SOURCE})
|
|
37
|
+
/x.freeze
|
|
38
|
+
|
|
39
|
+
attr_reader :provider, :repo, :directory, :branch, :hostname, :api_endpoint
|
|
40
|
+
|
|
41
|
+
def self.from_url(url_string)
|
|
42
|
+
return unless url_string&.match?(SOURCE_REGEX)
|
|
43
|
+
|
|
44
|
+
captures = url_string.match(SOURCE_REGEX).named_captures
|
|
45
|
+
|
|
46
|
+
new(
|
|
47
|
+
provider: captures.fetch("provider"),
|
|
48
|
+
repo: captures.fetch("repo"),
|
|
49
|
+
directory: captures.fetch("directory"),
|
|
50
|
+
branch: captures.fetch("branch")
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(provider:, repo:, directory: nil, branch: nil, hostname: nil,
|
|
55
|
+
api_endpoint: nil)
|
|
56
|
+
if hostname.nil? ^ api_endpoint.nil?
|
|
57
|
+
msg = "Both hostname and api_endpoint must be specified if either "\
|
|
58
|
+
"are. Alternatively, both may be left blank to use the "\
|
|
59
|
+
"provider's defaults."
|
|
60
|
+
raise msg
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@provider = provider
|
|
64
|
+
@repo = repo
|
|
65
|
+
@directory = directory
|
|
66
|
+
@branch = branch
|
|
67
|
+
@hostname = hostname || default_hostname(provider)
|
|
68
|
+
@api_endpoint = api_endpoint || default_api_endpoint(provider)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def url
|
|
72
|
+
case provider
|
|
73
|
+
when "github" then "https://github.com/" + repo
|
|
74
|
+
when "bitbucket" then "https://bitbucket.org/" + repo
|
|
75
|
+
when "gitlab" then "https://gitlab.com/" + repo
|
|
76
|
+
when "azure" then "https://dev.azure.com/" + repo
|
|
77
|
+
else raise "Unexpected repo provider '#{provider}'"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def organization
|
|
82
|
+
repo.split("/").first
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def project
|
|
86
|
+
raise "Project is an Azure DevOps concept only" unless provider == "azure"
|
|
87
|
+
|
|
88
|
+
parts = repo.split("/_git/")
|
|
89
|
+
return parts.first.split("/").last if parts.first.split("/").count == 2
|
|
90
|
+
|
|
91
|
+
parts.last
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unscoped_repo
|
|
95
|
+
repo.split("/").last
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def default_hostname(provider)
|
|
101
|
+
case provider
|
|
102
|
+
when "github" then "github.com"
|
|
103
|
+
when "bitbucket" then "bitbucket.org"
|
|
104
|
+
when "gitlab" then "gitlab.com"
|
|
105
|
+
when "azure" then "dev.azure.com"
|
|
106
|
+
else raise "Unexpected provider '#{provider}'"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def default_api_endpoint(provider)
|
|
111
|
+
case provider
|
|
112
|
+
when "github" then "https://api.github.com/"
|
|
113
|
+
when "bitbucket" then "https://api.bitbucket.org/2.0/"
|
|
114
|
+
when "gitlab" then "https://gitlab.com/api/v4"
|
|
115
|
+
when "azure" then "https://dev.azure.com/"
|
|
116
|
+
else raise "Unexpected provider '#{provider}'"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|