mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 10:53:43 +08:00
d2e4b32c87
Why this change? Currently, we do not have an easy way to test themes and theme components using Rails system tests. While we support QUnit acceptance tests for themes and theme components, QUnit acceptance tests stubs out the server and setting up the fixtures for server responses is difficult and can lead to a frustrating experience. System tests on the other hand allow authors to set up the test fixtures using our fabricator system which is much easier to use. What does this change do? In order for us to allow authors to run system tests with their themes installed, we are adding a `upload_theme` helper that is made available when writing system tests. The `upload_theme` helper requires a single `directory` parameter where `directory` is the directory of the theme locally and returns a `Theme` record.
170 lines
4.1 KiB
Ruby
170 lines
4.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ThemeStore::GitImporter < ThemeStore::BaseImporter
|
|
COMMAND_TIMEOUT_SECONDS = 20
|
|
|
|
attr_reader :url
|
|
|
|
def initialize(url, private_key: nil, branch: nil)
|
|
@url = GitUrl.normalize(url)
|
|
@private_key = private_key
|
|
@branch = branch
|
|
end
|
|
|
|
def import!
|
|
clone!
|
|
|
|
if version = Discourse.find_compatible_git_resource(temp_folder)
|
|
begin
|
|
execute "git", "cat-file", "-e", version
|
|
rescue RuntimeError => e
|
|
tracking_ref =
|
|
execute "git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"
|
|
remote_name = tracking_ref.split("/", 2)[0]
|
|
execute "git", "fetch", remote_name, "#{version}:#{version}"
|
|
end
|
|
|
|
begin
|
|
execute "git", "reset", "--hard", version
|
|
rescue RuntimeError
|
|
raise RemoteTheme::ImportError.new(
|
|
I18n.t("themes.import_error.git_ref_not_found", ref: version),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def commits_since(hash)
|
|
commit_hash, commits_behind = nil
|
|
|
|
commit_hash = execute("git", "rev-parse", "HEAD").strip
|
|
commits_behind =
|
|
begin
|
|
execute("git", "rev-list", "#{hash}..HEAD", "--count").strip
|
|
rescue StandardError
|
|
-1
|
|
end
|
|
|
|
[commit_hash, commits_behind]
|
|
end
|
|
|
|
def version
|
|
execute("git", "rev-parse", "HEAD").strip
|
|
end
|
|
|
|
protected
|
|
|
|
def redirected_uri
|
|
first_clone_uri = @uri.dup
|
|
first_clone_uri.path.gsub!(%r{/\z}, "")
|
|
first_clone_uri.path += "/info/refs"
|
|
first_clone_uri.query = "service=git-upload-pack"
|
|
|
|
redirected_uri = FinalDestination.resolve(first_clone_uri.to_s, http_verb: :get)
|
|
|
|
if redirected_uri&.path.ends_with?("/info/refs")
|
|
redirected_uri.path.gsub!(%r{/info/refs\z}, "")
|
|
redirected_uri.query = nil
|
|
redirected_uri
|
|
else
|
|
@uri
|
|
end
|
|
rescue StandardError
|
|
@uri
|
|
end
|
|
|
|
def raise_import_error!
|
|
raise RemoteTheme::ImportError.new(I18n.t("themes.import_error.git"))
|
|
end
|
|
|
|
def clone!
|
|
begin
|
|
@uri = URI.parse(@url)
|
|
rescue URI::Error
|
|
raise_import_error!
|
|
end
|
|
|
|
case @uri&.scheme
|
|
when "http", "https"
|
|
clone_http!
|
|
when "ssh"
|
|
clone_ssh!
|
|
else
|
|
raise RemoteTheme::ImportError.new(I18n.t("themes.import_error.git_unsupported_scheme"))
|
|
end
|
|
end
|
|
|
|
def clone_args(url, config = {})
|
|
args = ["git"]
|
|
|
|
config.each { |key, value| args.concat(["-c", "#{key}=#{value}"]) }
|
|
|
|
args << "clone"
|
|
|
|
args.concat(["--single-branch", "-b", @branch]) if @branch.present?
|
|
|
|
args.concat([url, temp_folder])
|
|
|
|
args
|
|
end
|
|
|
|
def clone_http!
|
|
uri = redirected_uri
|
|
|
|
raise_import_error! unless %w[http https].include?(@uri.scheme)
|
|
|
|
addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(uri.host)
|
|
|
|
unless addresses.empty?
|
|
env = { "GIT_TERMINAL_PROMPT" => "0" }
|
|
|
|
args =
|
|
clone_args(
|
|
uri.to_s,
|
|
"http.followRedirects" => "false",
|
|
"http.curloptResolve" => "#{uri.host}:#{uri.port}:#{addresses.join(",")}",
|
|
)
|
|
|
|
begin
|
|
Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS)
|
|
rescue RuntimeError
|
|
end
|
|
end
|
|
end
|
|
|
|
def clone_ssh!
|
|
raise_import_error! unless @private_key.present?
|
|
|
|
with_ssh_private_key do |ssh_folder|
|
|
# Use only the specified SSH key
|
|
env = {
|
|
"GIT_SSH_COMMAND" =>
|
|
"ssh -i #{ssh_folder}/id_rsa -o IdentitiesOnly=yes -o IdentityFile=#{ssh_folder}/id_rsa -o StrictHostKeyChecking=no",
|
|
}
|
|
args = clone_args(@url)
|
|
|
|
begin
|
|
Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS)
|
|
rescue RuntimeError
|
|
raise_import_error!
|
|
end
|
|
end
|
|
end
|
|
|
|
def with_ssh_private_key
|
|
ssh_folder = "#{Pathname.new(Dir.tmpdir).realpath}/discourse_theme_ssh_#{SecureRandom.hex}"
|
|
FileUtils.mkdir_p ssh_folder
|
|
|
|
File.write("#{ssh_folder}/id_rsa", @private_key)
|
|
FileUtils.chmod(0600, "#{ssh_folder}/id_rsa")
|
|
|
|
yield ssh_folder
|
|
ensure
|
|
FileUtils.rm_rf ssh_folder
|
|
end
|
|
|
|
def execute(*args)
|
|
Discourse::Utils.execute_command(*args, chdir: temp_folder, timeout: COMMAND_TIMEOUT_SECONDS)
|
|
end
|
|
end
|