2020-03-30 00:47:38 +11:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-04-14 00:10:02 +10:00
|
|
|
require "download_strategy"
|
2020-03-30 00:47:38 +11:00
|
|
|
require "cli/parser"
|
|
|
|
require "utils/github"
|
|
|
|
require "tmpdir"
|
|
|
|
require "bintray"
|
|
|
|
|
|
|
|
module Homebrew
|
|
|
|
module_function
|
|
|
|
|
|
|
|
def pr_pull_args
|
|
|
|
Homebrew::CLI::Parser.new do
|
|
|
|
usage_banner <<~EOS
|
2020-04-12 12:05:50 +02:00
|
|
|
`pr-pull` [<options>] <pull_request> [<pull_request> ...]
|
2020-03-30 00:47:38 +11:00
|
|
|
|
|
|
|
Download and publish bottles, and apply the bottle commit from a
|
2020-04-18 12:13:43 -04:00
|
|
|
pull request with artifacts generated by GitHub Actions.
|
2020-03-30 00:47:38 +11:00
|
|
|
Requires write access to the repository.
|
|
|
|
EOS
|
|
|
|
switch "--no-publish",
|
2020-04-18 12:13:43 -04:00
|
|
|
description: "Download the bottles, apply the bottle commit and "\
|
2020-03-30 00:47:38 +11:00
|
|
|
"upload the bottles to Bintray, but don't publish them."
|
|
|
|
switch "--no-upload",
|
|
|
|
description: "Download the bottles and apply the bottle commit, "\
|
|
|
|
"but don't upload to Bintray."
|
2020-04-18 12:13:43 -04:00
|
|
|
switch "-n", "--dry-run",
|
2020-03-30 00:47:38 +11:00
|
|
|
description: "Print what would be done rather than doing it."
|
|
|
|
switch "--clean",
|
|
|
|
description: "Do not amend the commits from pull requests."
|
|
|
|
switch "--branch-okay",
|
|
|
|
description: "Do not warn if pulling to a branch besides master (useful for testing)."
|
|
|
|
switch "--resolve",
|
2020-04-18 12:13:43 -04:00
|
|
|
description: "When a patch fails to apply, leave in progress and allow user to resolve, "\
|
|
|
|
"instead of aborting."
|
|
|
|
flag "--workflow=",
|
|
|
|
description: "Retrieve artifacts from the specified workflow (default: tests.yml)."
|
|
|
|
flag "--artifact=",
|
|
|
|
description: "Download artifacts with the specified name (default: bottles)."
|
|
|
|
flag "--bintray-org=",
|
|
|
|
description: "Upload to the specified Bintray organisation (default: homebrew)."
|
|
|
|
flag "--tap=",
|
|
|
|
description: "Target tap repository (default: homebrew/core)."
|
2020-03-30 00:47:38 +11:00
|
|
|
switch :verbose
|
|
|
|
switch :debug
|
|
|
|
min_named 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-31 22:11:30 +11:00
|
|
|
def setup_git_environment!
|
|
|
|
# Passthrough Git environment variables
|
|
|
|
ENV["GIT_COMMITTER_NAME"] = ENV["HOMEBREW_GIT_NAME"] if ENV["HOMEBREW_GIT_NAME"]
|
|
|
|
ENV["GIT_COMMITTER_EMAIL"] = ENV["HOMEBREW_GIT_EMAIL"] if ENV["HOMEBREW_GIT_EMAIL"]
|
|
|
|
|
|
|
|
# Depending on user configuration, git may try to invoke gpg.
|
|
|
|
return unless Utils.popen_read("git config --get --bool commit.gpgsign").chomp == "true"
|
|
|
|
|
|
|
|
begin
|
|
|
|
gnupg = Formula["gnupg"]
|
|
|
|
rescue FormulaUnavailableError
|
|
|
|
nil
|
|
|
|
else
|
2020-05-18 13:50:43 +01:00
|
|
|
if gnupg.any_version_installed?
|
2020-03-31 22:11:30 +11:00
|
|
|
path = PATH.new(ENV.fetch("PATH"))
|
|
|
|
path.prepend(gnupg.installed_prefix/"bin")
|
|
|
|
ENV["PATH"] = path
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-11 19:26:32 +10:00
|
|
|
def signoff!(pr, path: ".")
|
2020-03-30 00:47:38 +11:00
|
|
|
message = Utils.popen_read "git", "-C", path, "log", "-1", "--pretty=%B"
|
|
|
|
close_message = "Closes ##{pr}."
|
|
|
|
message += "\n#{close_message}" unless message.include? close_message
|
2020-04-11 19:26:32 +10:00
|
|
|
if Homebrew.args.dry_run?
|
2020-03-30 00:47:38 +11:00
|
|
|
puts "git commit --amend --signoff -m $message"
|
|
|
|
else
|
|
|
|
safe_system "git", "-C", path, "commit", "--amend", "--signoff", "--allow-empty", "-q", "-m", message
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-11 19:26:32 +10:00
|
|
|
def cherry_pick_pr!(pr, path: ".")
|
|
|
|
if Homebrew.args.dry_run?
|
2020-03-30 00:47:38 +11:00
|
|
|
puts <<~EOS
|
|
|
|
git fetch --force origin +refs/pull/#{pr}/head
|
|
|
|
git merge-base HEAD FETCH_HEAD
|
|
|
|
git cherry-pick --ff --allow-empty $merge_base..FETCH_HEAD
|
|
|
|
EOS
|
|
|
|
else
|
|
|
|
safe_system "git", "-C", path, "fetch", "--quiet", "--force", "origin", "+refs/pull/#{pr}/head"
|
|
|
|
merge_base = Utils.popen_read("git", "-C", path, "merge-base", "HEAD", "FETCH_HEAD").strip
|
|
|
|
commit_count = Utils.popen_read("git", "-C", path, "rev-list", "#{merge_base}..FETCH_HEAD").lines.count
|
|
|
|
|
|
|
|
# git cherry-pick unfortunately has no quiet option
|
|
|
|
ohai "Cherry-picking #{commit_count} commit#{"s" unless commit_count == 1} from ##{pr}"
|
|
|
|
cherry_pick_args = "git", "-C", path, "cherry-pick", "--ff", "--allow-empty", "#{merge_base}..FETCH_HEAD"
|
|
|
|
result = Homebrew.args.verbose? ? system(*cherry_pick_args) : quiet_system(*cherry_pick_args)
|
|
|
|
|
|
|
|
unless result
|
|
|
|
if Homebrew.args.resolve?
|
|
|
|
odie "Cherry-pick failed: try to resolve it."
|
|
|
|
else
|
|
|
|
system "git", "-C", path, "cherry-pick", "--abort"
|
|
|
|
odie "Cherry-pick failed!"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_branch(path, ref)
|
|
|
|
branch = Utils.popen_read("git", "-C", path, "symbolic-ref", "--short", "HEAD").strip
|
|
|
|
|
|
|
|
return if branch == ref || args.clean? || args.branch_okay?
|
|
|
|
|
|
|
|
opoo "Current branch is #{branch}: do you need to pull inside #{ref}?"
|
|
|
|
end
|
|
|
|
|
2020-04-11 19:27:34 +10:00
|
|
|
def formulae_need_bottles?(tap, original_commit)
|
|
|
|
return if Homebrew.args.dry_run?
|
|
|
|
|
|
|
|
if Homebrew::EnvConfig.disable_load_formula?
|
|
|
|
opoo "Can't check if updated bottles are necessary as formula loading is disabled!"
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
Utils.popen_read("git", "-C", tap.path, "diff-tree",
|
|
|
|
"-r", "--name-only", "--diff-filter=AM",
|
|
|
|
original_commit, "HEAD", "--", tap.formula_dir)
|
|
|
|
.lines.each do |line|
|
|
|
|
next unless line.end_with? ".rb\n"
|
|
|
|
|
|
|
|
name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
|
|
|
|
begin
|
|
|
|
f = Formula[name]
|
|
|
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
|
|
# Make sure we catch syntax errors.
|
|
|
|
next
|
|
|
|
end
|
|
|
|
return true if !f.bottle_unneeded? && !f.bottle_disabled?
|
|
|
|
end
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2020-04-14 00:10:02 +10:00
|
|
|
def download_artifact(url, dir, pr)
|
|
|
|
token, username = GitHub.api_credentials
|
|
|
|
case GitHub.api_credentials_type
|
|
|
|
when :env_username_password, :keychain_username_password
|
|
|
|
curl_args = ["--user", "#{username}:#{token}"]
|
|
|
|
when :env_token
|
|
|
|
curl_args = ["--header", "Authorization: token #{token}"]
|
|
|
|
when :none
|
|
|
|
raise Error, "Credentials must be set to access the Artifacts API"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Download the artifact as a zip file and unpack it into `dir`. This is
|
|
|
|
# preferred over system `curl` and `tar` as this leverages the Homebrew
|
|
|
|
# cache to avoid repeated downloads of (possibly large) bottles.
|
|
|
|
FileUtils.chdir dir do
|
|
|
|
downloader = GitHubArtifactDownloadStrategy.new(url, "artifact", pr, curl_args: curl_args, secrets: [token])
|
|
|
|
downloader.fetch
|
|
|
|
downloader.stage
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-30 00:47:38 +11:00
|
|
|
def pr_pull
|
|
|
|
pr_pull_args.parse
|
|
|
|
|
|
|
|
bintray_user = ENV["HOMEBREW_BINTRAY_USER"]
|
|
|
|
bintray_key = ENV["HOMEBREW_BINTRAY_KEY"]
|
2020-03-31 22:11:30 +11:00
|
|
|
bintray_org = args.bintray_org || "homebrew"
|
2020-03-30 00:47:38 +11:00
|
|
|
|
|
|
|
if bintray_user.blank? || bintray_key.blank?
|
|
|
|
odie "Missing HOMEBREW_BINTRAY_USER or HOMEBREW_BINTRAY_KEY variables!" if !args.dry_run? && !args.no_upload?
|
|
|
|
else
|
2020-03-31 22:11:30 +11:00
|
|
|
bintray = Bintray.new(user: bintray_user, key: bintray_key, org: bintray_org)
|
2020-03-30 00:47:38 +11:00
|
|
|
end
|
|
|
|
|
|
|
|
workflow = args.workflow || "tests.yml"
|
|
|
|
artifact = args.artifact || "bottles"
|
2020-04-14 00:10:02 +10:00
|
|
|
tap = Tap.fetch(args.tap || CoreTap.instance.name)
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2020-03-31 22:11:30 +11:00
|
|
|
setup_git_environment!
|
|
|
|
|
2020-04-12 12:05:50 +02:00
|
|
|
args.named.uniq.each do |arg|
|
2020-03-31 11:24:10 +02:00
|
|
|
arg = "#{tap.default_remote}/pull/#{arg}" if arg.to_i.positive?
|
2020-03-30 00:47:38 +11:00
|
|
|
url_match = arg.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX
|
|
|
|
_, user, repo, pr = *url_match
|
|
|
|
odie "Not a GitHub pull request: #{arg}" unless pr
|
|
|
|
|
|
|
|
check_branch tap.path, "master"
|
|
|
|
|
|
|
|
ohai "Fetching #{tap} pull request ##{pr}"
|
|
|
|
Dir.mktmpdir pr do |dir|
|
|
|
|
cd dir do
|
2020-04-11 19:27:34 +10:00
|
|
|
original_commit = Utils.popen_read("git", "-C", tap.path, "rev-parse", "HEAD").chomp
|
2020-04-11 19:26:32 +10:00
|
|
|
cherry_pick_pr! pr, path: tap.path
|
|
|
|
signoff! pr, path: tap.path unless args.clean?
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2020-04-11 19:27:34 +10:00
|
|
|
unless formulae_need_bottles? tap, original_commit
|
|
|
|
ohai "Skipping artifacts for ##{pr} as the formulae don't need bottles"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
2020-04-14 00:10:02 +10:00
|
|
|
url = GitHub.get_artifact_url(user, repo, pr, workflow_id: workflow, artifact_name: artifact)
|
|
|
|
download_artifact(url, dir, pr)
|
2020-04-11 19:27:34 +10:00
|
|
|
|
2020-04-11 19:26:32 +10:00
|
|
|
if Homebrew.args.dry_run?
|
2020-03-30 00:47:38 +11:00
|
|
|
puts "brew bottle --merge --write #{Dir["*.json"].join " "}"
|
|
|
|
else
|
|
|
|
quiet_system "#{HOMEBREW_PREFIX}/bin/brew", "bottle", "--merge", "--write", *Dir["*.json"]
|
|
|
|
end
|
|
|
|
|
|
|
|
next if args.no_upload?
|
|
|
|
|
2020-04-11 19:26:32 +10:00
|
|
|
if Homebrew.args.dry_run?
|
2020-03-30 00:47:38 +11:00
|
|
|
puts "Upload bottles described by these JSON files to Bintray:\n #{Dir["*.json"].join("\n ")}"
|
|
|
|
else
|
2020-04-13 12:54:06 +01:00
|
|
|
bintray.upload_bottle_json Dir["*.json"], publish_package: !args.no_publish?
|
2020-03-30 00:47:38 +11:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-04-14 00:10:02 +10:00
|
|
|
|
|
|
|
class GitHubArtifactDownloadStrategy < AbstractFileDownloadStrategy
|
|
|
|
def fetch
|
|
|
|
ohai "Downloading #{url}"
|
|
|
|
if cached_location.exist?
|
|
|
|
puts "Already downloaded: #{cached_location}"
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
curl "--location", "--create-dirs", "--output", temporary_path, url,
|
|
|
|
*meta.fetch(:curl_args, []),
|
|
|
|
secrets: meta.fetch(:secrets, [])
|
|
|
|
rescue ErrorDuringExecution
|
|
|
|
raise CurlDownloadStrategyError, url
|
|
|
|
end
|
|
|
|
ignore_interrupts do
|
|
|
|
cached_location.dirname.mkpath
|
|
|
|
temporary_path.rename(cached_location)
|
|
|
|
symlink_location.dirname.mkpath
|
|
|
|
end
|
|
|
|
end
|
|
|
|
FileUtils.ln_s cached_location.relative_path_from(symlink_location.dirname), symlink_location, force: true
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def resolved_basename
|
|
|
|
"artifact.zip"
|
|
|
|
end
|
|
|
|
end
|