2024-07-01 18:07:47 +01:00
|
|
|
# typed: strict
|
2020-03-30 00:47:38 +11:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
require "abstract_command"
|
|
|
|
require "fileutils"
|
2020-03-30 00:47:38 +11:00
|
|
|
require "utils/github"
|
2023-05-17 23:48:58 +08:00
|
|
|
require "utils/github/artifacts"
|
2020-03-30 00:47:38 +11:00
|
|
|
require "tmpdir"
|
2020-08-22 14:21:02 +10:00
|
|
|
require "formula"
|
2020-03-30 00:47:38 +11:00
|
|
|
|
|
|
|
module Homebrew
|
2024-03-21 21:31:25 -07:00
|
|
|
module DevCmd
|
|
|
|
class PrPull < AbstractCommand
|
|
|
|
include FileUtils
|
|
|
|
|
|
|
|
cmd_args do
|
|
|
|
description <<~EOS
|
2024-04-30 11:10:23 +02:00
|
|
|
Download and publish bottles and apply the bottle commit from a
|
2024-03-21 21:31:25 -07:00
|
|
|
pull request with artifacts generated by GitHub Actions.
|
|
|
|
Requires write access to the repository.
|
|
|
|
EOS
|
|
|
|
switch "--no-upload",
|
|
|
|
description: "Download the bottles but don't upload them."
|
|
|
|
switch "--no-commit",
|
|
|
|
description: "Do not generate a new commit before uploading."
|
|
|
|
switch "--no-cherry-pick",
|
|
|
|
description: "Do not cherry-pick commits from the pull request branch."
|
|
|
|
switch "-n", "--dry-run",
|
|
|
|
description: "Print what would be done rather than doing it."
|
|
|
|
switch "--clean",
|
|
|
|
description: "Do not amend the commits from pull requests."
|
|
|
|
switch "--keep-old",
|
|
|
|
description: "If the formula specifies a rebuild version, " \
|
|
|
|
"attempt to preserve its value in the generated DSL."
|
|
|
|
switch "--autosquash",
|
|
|
|
description: "Automatically reformat and reword commits in the pull request to our " \
|
|
|
|
"preferred format."
|
|
|
|
switch "--branch-okay",
|
|
|
|
description: "Do not warn if pulling to a branch besides the repository default (useful for testing)."
|
|
|
|
switch "--resolve",
|
|
|
|
description: "When a patch fails to apply, leave in progress and allow user to resolve, " \
|
|
|
|
"instead of aborting."
|
|
|
|
switch "--warn-on-upload-failure",
|
|
|
|
description: "Warn instead of raising an error if the bottle upload fails. " \
|
|
|
|
"Useful for repairing bottle uploads that previously failed."
|
|
|
|
switch "--retain-bottle-dir",
|
|
|
|
description: "Does not clean up the tmp directory for the bottle so it can be used later."
|
|
|
|
flag "--committer=",
|
|
|
|
description: "Specify a committer name and email in `git`'s standard author format."
|
|
|
|
flag "--message=",
|
|
|
|
depends_on: "--autosquash",
|
|
|
|
description: "Message to include when autosquashing revision bumps, deletions and rebuilds."
|
2024-04-17 03:32:48 +08:00
|
|
|
flag "--artifact-pattern=", "--artifact=",
|
2024-04-17 05:52:11 +08:00
|
|
|
description: "Download artifacts with the specified pattern (default: `bottles{,_*}`)."
|
2024-03-21 21:31:25 -07:00
|
|
|
flag "--tap=",
|
|
|
|
description: "Target tap repository (default: `homebrew/core`)."
|
|
|
|
flag "--root-url=",
|
|
|
|
description: "Use the specified <URL> as the root of the bottle's URL instead of Homebrew's default."
|
|
|
|
flag "--root-url-using=",
|
|
|
|
description: "Use the specified download strategy class for downloading the bottle's URL instead of " \
|
|
|
|
"Homebrew's default."
|
|
|
|
comma_array "--workflows",
|
|
|
|
description: "Retrieve artifacts from the specified workflow (default: `tests.yml`). " \
|
|
|
|
"Can be a comma-separated list to include multiple workflows."
|
|
|
|
comma_array "--ignore-missing-artifacts",
|
|
|
|
description: "Comma-separated list of workflows which can be ignored if they have not been run."
|
|
|
|
|
|
|
|
conflicts "--clean", "--autosquash"
|
|
|
|
|
|
|
|
named_args :pull_request, min: 1
|
|
|
|
end
|
2020-06-27 23:00:05 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
sig { override.void }
|
|
|
|
def run
|
|
|
|
# Needed when extracting the CI artifact.
|
|
|
|
ensure_executable!("unzip", reason: "extracting CI artifacts")
|
2020-06-10 19:27:05 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
workflows = args.workflows.presence || ["tests.yml"]
|
2024-04-17 05:52:11 +08:00
|
|
|
artifact_pattern = args.artifact_pattern || "bottles{,_*}"
|
2024-03-21 21:31:25 -07:00
|
|
|
tap = Tap.fetch(args.tap || CoreTap.instance.name)
|
|
|
|
raise TapUnavailableError, tap.name unless tap.installed?
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
Utils::Git.set_name_email!(committer: args.committer.blank?)
|
|
|
|
Utils::Git.setup_gpg!
|
2020-09-19 17:14:06 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
if (committer = args.committer)
|
|
|
|
committer = Utils.parse_author!(committer)
|
|
|
|
ENV["GIT_COMMITTER_NAME"] = committer[:name]
|
|
|
|
ENV["GIT_COMMITTER_EMAIL"] = committer[:email]
|
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
args.named.uniq.each do |arg|
|
|
|
|
arg = "#{tap.default_remote}/pull/#{arg}" if arg.to_i.positive?
|
|
|
|
url_match = arg.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX
|
|
|
|
_, user, repo, pr = *url_match
|
|
|
|
odie "Not a GitHub pull request: #{arg}" unless pr
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-06-10 09:31:53 +01:00
|
|
|
git_repo = tap.git_repository
|
2024-03-21 21:31:25 -07:00
|
|
|
if !git_repo.default_origin_branch? && !args.branch_okay? && !args.no_commit? && !args.no_cherry_pick?
|
|
|
|
origin_branch_name = git_repo.origin_branch_name
|
|
|
|
opoo "Current branch is #{git_repo.branch_name}: do you need to pull inside #{origin_branch_name}?"
|
|
|
|
end
|
2020-06-10 19:27:05 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
pr_labels = GitHub.pull_request_labels(user, repo, pr)
|
|
|
|
if pr_labels.include?("autosquash") && !args.autosquash?
|
|
|
|
opoo "Pull request is labelled `autosquash`: do you need to pass `--autosquash`?"
|
|
|
|
end
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
pr_check_conflicts("#{user}/#{repo}", pr)
|
|
|
|
|
|
|
|
ohai "Fetching #{tap} pull request ##{pr}"
|
|
|
|
dir = Dir.mktmpdir("pr-pull-#{pr}-", HOMEBREW_TEMP)
|
|
|
|
begin
|
|
|
|
cd dir do
|
|
|
|
current_branch_head = ENV["GITHUB_SHA"] || tap.git_head
|
|
|
|
original_commit = if args.no_cherry_pick?
|
|
|
|
# TODO: Handle the case where `merge-base` returns multiple commits.
|
|
|
|
Utils.safe_popen_read("git", "-C", tap.path, "merge-base", "origin/HEAD",
|
|
|
|
current_branch_head).strip
|
|
|
|
else
|
|
|
|
current_branch_head
|
|
|
|
end
|
|
|
|
odebug "Pull request merge-base: #{original_commit}"
|
|
|
|
|
|
|
|
unless args.no_commit?
|
|
|
|
cherry_pick_pr!(user, repo, pr, path: tap.path) unless args.no_cherry_pick?
|
|
|
|
if args.autosquash? && !args.dry_run?
|
|
|
|
autosquash!(original_commit, tap:, cherry_picked: !args.no_cherry_pick?,
|
2024-07-02 15:24:01 +01:00
|
|
|
verbose: args.verbose?, resolve: args.resolve?, reason: args.message)
|
2024-03-21 21:31:25 -07:00
|
|
|
end
|
|
|
|
signoff!(git_repo, pull_request: pr, dry_run: args.dry_run?) unless args.clean?
|
|
|
|
end
|
|
|
|
|
|
|
|
unless formulae_need_bottles?(tap, original_commit, pr_labels)
|
|
|
|
ohai "Skipping artifacts for ##{pr} as the formulae don't need bottles"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
workflows.each do |workflow|
|
|
|
|
workflow_run = GitHub.get_workflow_run(
|
2024-04-17 03:32:48 +08:00
|
|
|
user, repo, pr, workflow_id: workflow, artifact_pattern:
|
2024-03-21 21:31:25 -07:00
|
|
|
)
|
|
|
|
if args.ignore_missing_artifacts.present? &&
|
2024-07-02 15:24:01 +01:00
|
|
|
args.ignore_missing_artifacts&.include?(workflow) &&
|
2024-03-21 21:31:25 -07:00
|
|
|
workflow_run.first.blank?
|
|
|
|
# Ignore that workflow as it was not executed and we specified
|
|
|
|
# that we could skip it.
|
|
|
|
ohai "Ignoring workflow #{workflow} as requested by `--ignore-missing-artifacts`"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
ohai "Downloading bottles for workflow: #{workflow}"
|
2024-04-17 05:52:11 +08:00
|
|
|
|
2024-04-17 03:32:48 +08:00
|
|
|
urls = GitHub.get_artifact_urls(workflow_run)
|
2024-04-17 07:37:02 +08:00
|
|
|
urls.each { |url| GitHub.download_artifact(url, pr) }
|
2024-03-21 21:31:25 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
next if args.no_upload?
|
|
|
|
|
|
|
|
upload_args = ["pr-upload"]
|
|
|
|
upload_args << "--debug" if args.debug?
|
|
|
|
upload_args << "--verbose" if args.verbose?
|
|
|
|
upload_args << "--no-commit" if args.no_commit?
|
|
|
|
upload_args << "--dry-run" if args.dry_run?
|
|
|
|
upload_args << "--keep-old" if args.keep_old?
|
|
|
|
upload_args << "--warn-on-upload-failure" if args.warn_on_upload_failure?
|
|
|
|
upload_args << "--committer=#{args.committer}" if args.committer
|
|
|
|
upload_args << "--root-url=#{args.root_url}" if args.root_url
|
|
|
|
upload_args << "--root-url-using=#{args.root_url_using}" if args.root_url_using
|
|
|
|
safe_system HOMEBREW_BREW_FILE, *upload_args
|
|
|
|
end
|
|
|
|
ensure
|
2024-05-09 13:19:14 +01:00
|
|
|
if args.retain_bottle_dir? && GitHub::Actions.env_set?
|
2024-03-21 21:31:25 -07:00
|
|
|
ohai "Bottle files retained at:", dir
|
|
|
|
File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f|
|
|
|
|
f.puts "bottle_path=#{dir}"
|
|
|
|
end
|
|
|
|
else
|
|
|
|
FileUtils.remove_entry dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2022-01-10 18:57:56 +02:00
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-04-30 11:10:23 +02:00
|
|
|
# Separates a commit message into subject, body and trailers.
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(message: String).returns([String, String, String]) }
|
2024-03-21 21:31:25 -07:00
|
|
|
def separate_commit_message(message)
|
2024-07-02 15:24:01 +01:00
|
|
|
first_line = message.lines.first
|
|
|
|
return ["", "", ""] unless first_line
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Skip the subject and separate lines that look like trailers (e.g. "Co-authored-by")
|
|
|
|
# from lines that look like regular body text.
|
|
|
|
trailers, body = message.lines.drop(1).partition { |s| s.match?(/^[a-z-]+-by:/i) }
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
trailers = trailers.uniq.join.strip
|
|
|
|
body = body.join.strip.gsub(/\n{3,}/, "\n\n")
|
2020-09-19 15:22:02 +10:00
|
|
|
|
2024-07-02 15:24:01 +01:00
|
|
|
[first_line.strip, body, trailers]
|
2024-03-21 21:31:25 -07:00
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(git_repo: GitRepository, pull_request: T.nilable(String), dry_run: T::Boolean).void }
|
2024-03-21 21:31:25 -07:00
|
|
|
def signoff!(git_repo, pull_request: nil, dry_run: false)
|
2024-07-02 15:24:01 +01:00
|
|
|
msg = git_repo.commit_message
|
|
|
|
return if msg.blank?
|
|
|
|
|
|
|
|
subject, body, trailers = separate_commit_message(msg)
|
2020-09-19 15:22:02 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
if pull_request
|
|
|
|
# This is a tap pull request and approving reviewers should also sign-off.
|
|
|
|
tap = Tap.from_path(git_repo.pathname)
|
|
|
|
review_trailers = GitHub.approved_reviews(tap.user, tap.full_name.split("/").last,
|
|
|
|
pull_request).map do |r|
|
|
|
|
"Signed-off-by: #{r["name"]} <#{r["email"]}>"
|
|
|
|
end
|
|
|
|
trailers = trailers.lines.concat(review_trailers).map(&:strip).uniq.join("\n")
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Append the close message as well, unless the commit body already includes it.
|
|
|
|
close_message = "Closes ##{pull_request}."
|
2024-07-01 18:07:47 +01:00
|
|
|
body.concat("\n\n#{close_message}") unless body.include?(close_message)
|
2024-03-21 21:31:25 -07:00
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
git_args = Utils::Git.git, "-C", git_repo.pathname, "commit", "--amend", "--signoff", "--allow-empty",
|
|
|
|
"--quiet", "--message", subject, "--message", body, "--message", trailers
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
if dry_run
|
|
|
|
puts(*git_args)
|
|
|
|
else
|
|
|
|
safe_system(*git_args)
|
|
|
|
end
|
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(tap: Tap, subject_name: String, subject_path: Pathname, content: String).returns(T.untyped) }
|
2024-03-21 21:31:25 -07:00
|
|
|
def get_package(tap, subject_name, subject_path, content)
|
|
|
|
if subject_path.to_s.start_with?("#{tap.cask_dir}/")
|
|
|
|
cask = begin
|
|
|
|
Cask::CaskLoader.load(content.dup)
|
|
|
|
rescue Cask::CaskUnavailableError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
return cask
|
2022-01-10 18:57:56 +02:00
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
begin
|
|
|
|
Formulary.from_contents(subject_name, subject_path, content, :stable)
|
|
|
|
rescue FormulaUnavailableError
|
|
|
|
nil
|
|
|
|
end
|
2020-06-28 18:27:45 +10:00
|
|
|
end
|
|
|
|
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
|
|
|
params(old_contents: String, new_contents: String, subject_path: T.any(String, Pathname),
|
|
|
|
reason: T.nilable(String)).returns(String)
|
|
|
|
}
|
2024-03-21 21:31:25 -07:00
|
|
|
def determine_bump_subject(old_contents, new_contents, subject_path, reason: nil)
|
|
|
|
subject_path = Pathname(subject_path)
|
|
|
|
tap = Tap.from_path(subject_path)
|
|
|
|
subject_name = subject_path.basename.to_s.chomp(".rb")
|
|
|
|
is_cask = subject_path.to_s.start_with?("#{tap.cask_dir}/")
|
|
|
|
name = is_cask ? "cask" : "formula"
|
2020-06-28 18:27:45 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
new_package = get_package(tap, subject_name, subject_path, new_contents)
|
2020-09-17 16:06:41 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
return "#{subject_name}: delete #{reason}".strip if new_package.blank?
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
old_package = get_package(tap, subject_name, subject_path, old_contents)
|
2021-07-08 13:43:43 +05:30
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
if old_package.blank?
|
|
|
|
"#{subject_name} #{new_package.version} (new #{name})"
|
|
|
|
elsif old_package.version != new_package.version
|
|
|
|
"#{subject_name} #{new_package.version}"
|
|
|
|
elsif !is_cask && old_package.revision != new_package.revision
|
|
|
|
"#{subject_name}: revision #{reason}".strip
|
|
|
|
elsif is_cask && old_package.sha256 != new_package.sha256
|
|
|
|
"#{subject_name}: checksum update #{reason}".strip
|
|
|
|
else
|
|
|
|
"#{subject_name}: #{reason || "rebuild"}".strip
|
|
|
|
end
|
|
|
|
end
|
2020-04-11 19:27:34 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Cherry picks a single commit that modifies a single file.
|
|
|
|
# Potentially rewords this commit using {determine_bump_subject}.
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
|
|
|
params(commit: String, file: String, git_repo: GitRepository, reason: T.nilable(String), verbose: T::Boolean,
|
|
|
|
resolve: T::Boolean).void
|
|
|
|
}
|
2024-03-21 21:31:25 -07:00
|
|
|
def reword_package_commit(commit, file, git_repo:, reason: "", verbose: false, resolve: false)
|
|
|
|
package_file = git_repo.pathname / file
|
|
|
|
package_name = package_file.basename.to_s.chomp(".rb")
|
2020-06-20 21:55:49 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
odebug "Cherry-picking #{package_file}: #{commit}"
|
|
|
|
Utils::Git.cherry_pick!(git_repo.to_s, commit, verbose:, resolve:)
|
2020-04-11 19:27:34 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
old_package = Utils::Git.file_at_commit(git_repo.to_s, file, "HEAD^")
|
|
|
|
new_package = Utils::Git.file_at_commit(git_repo.to_s, file, "HEAD")
|
2022-09-03 20:54:09 +01:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
bump_subject = determine_bump_subject(old_package, new_package, package_file, reason:).strip
|
2024-07-02 15:24:01 +01:00
|
|
|
msg = git_repo.commit_message
|
|
|
|
return if msg.blank?
|
|
|
|
|
|
|
|
subject, body, trailers = separate_commit_message(msg)
|
2022-08-05 21:41:28 +08:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
if subject != bump_subject && !subject.start_with?("#{package_name}:")
|
|
|
|
safe_system("git", "-C", git_repo.pathname, "commit", "--amend", "-q",
|
|
|
|
"-m", bump_subject, "-m", subject, "-m", body, "-m", trailers)
|
|
|
|
ohai bump_subject
|
|
|
|
else
|
|
|
|
ohai subject
|
|
|
|
end
|
2022-07-04 19:34:30 +02:00
|
|
|
end
|
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Cherry picks multiple commits that each modify a single file.
|
|
|
|
# Words the commit according to {determine_bump_subject} with the body
|
|
|
|
# corresponding to all the original commit messages combined.
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
|
|
|
params(commits: T::Array[String], file: String, git_repo: GitRepository, reason: T.nilable(String),
|
|
|
|
verbose: T::Boolean, resolve: T::Boolean).void
|
|
|
|
}
|
2024-03-21 21:31:25 -07:00
|
|
|
def squash_package_commits(commits, file, git_repo:, reason: "", verbose: false, resolve: false)
|
|
|
|
odebug "Squashing #{file}: #{commits.join " "}"
|
|
|
|
|
|
|
|
# Format commit messages into something similar to `git fmt-merge-message`.
|
|
|
|
# * subject 1
|
|
|
|
# * subject 2
|
|
|
|
# optional body
|
|
|
|
# * subject 3
|
|
|
|
messages = []
|
|
|
|
trailers = []
|
|
|
|
commits.each do |commit|
|
2024-07-02 15:24:01 +01:00
|
|
|
msg = git_repo.commit_message(commit)
|
|
|
|
next if msg.blank?
|
|
|
|
|
|
|
|
subject, body, trailer = separate_commit_message(msg)
|
2024-03-21 21:31:25 -07:00
|
|
|
body = body.lines.map { |line| " #{line.strip}" }.join("\n")
|
|
|
|
messages << "* #{subject}\n#{body}".strip
|
|
|
|
trailers << trailer
|
|
|
|
end
|
2022-09-03 20:54:09 +01:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Get the set of authors in this series.
|
|
|
|
authors = Utils.safe_popen_read("git", "-C", git_repo.pathname, "show",
|
|
|
|
"--no-patch", "--pretty=%an <%ae>", *commits).lines.map(&:strip).uniq.compact
|
|
|
|
|
|
|
|
# Get the author and date of the first commit of this series, which we use for the squashed commit.
|
|
|
|
original_author = authors.shift
|
|
|
|
original_date = Utils.safe_popen_read "git", "-C", git_repo.pathname, "show", "--no-patch", "--pretty=%ad",
|
|
|
|
commits.first
|
|
|
|
|
|
|
|
# Generate trailers for coauthors and combine them with the existing trailers.
|
|
|
|
co_author_trailers = authors.map { |au| "Co-authored-by: #{au}" }
|
|
|
|
trailers = [trailers + co_author_trailers].flatten.uniq.compact
|
|
|
|
|
|
|
|
# Apply the patch series but don't commit anything yet.
|
|
|
|
Utils::Git.cherry_pick!(git_repo.pathname, "--no-commit", *commits, verbose:, resolve:)
|
|
|
|
|
|
|
|
# Determine the bump subject by comparing the original state of the tree to its current state.
|
|
|
|
package_file = git_repo.pathname / file
|
|
|
|
old_package = Utils::Git.file_at_commit(git_repo.pathname, file, "#{commits.first}^")
|
|
|
|
new_package = package_file.read
|
|
|
|
bump_subject = determine_bump_subject(old_package, new_package, package_file, reason:)
|
|
|
|
|
2024-04-30 11:10:23 +02:00
|
|
|
# Commit with the new subject, body and trailers.
|
2024-03-21 21:31:25 -07:00
|
|
|
safe_system("git", "-C", git_repo.pathname, "commit", "--quiet",
|
|
|
|
"-m", bump_subject, "-m", messages.join("\n"), "-m", trailers.join("\n"),
|
|
|
|
"--author", original_author, "--date", original_date, "--", file)
|
|
|
|
ohai bump_subject
|
|
|
|
end
|
2022-07-04 19:34:30 +02:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# TODO: fix test in `test/dev-cmd/pr-pull_spec.rb` and assume `cherry_picked: false`.
|
2024-07-01 19:13:38 +01:00
|
|
|
sig {
|
2024-07-02 15:24:01 +01:00
|
|
|
params(original_commit: String, tap: Tap, reason: T.nilable(String), verbose: T::Boolean, resolve: T::Boolean,
|
2024-07-01 19:13:38 +01:00
|
|
|
cherry_picked: T::Boolean).void
|
|
|
|
}
|
2024-03-21 21:31:25 -07:00
|
|
|
def autosquash!(original_commit, tap:, reason: "", verbose: false, resolve: false, cherry_picked: true)
|
2024-06-10 09:31:53 +01:00
|
|
|
git_repo = tap.git_repository
|
2024-03-21 21:31:25 -07:00
|
|
|
|
|
|
|
commits = Utils.safe_popen_read("git", "-C", tap.path, "rev-list",
|
|
|
|
"--reverse", "#{original_commit}..HEAD").lines.map(&:strip)
|
|
|
|
|
|
|
|
# Generate a bidirectional mapping of commits <=> formula/cask files.
|
|
|
|
files_to_commits = {}
|
|
|
|
commits_to_files = commits.to_h do |commit|
|
|
|
|
files = Utils.safe_popen_read("git", "-C", tap.path, "diff-tree", "--diff-filter=AMD",
|
|
|
|
"-r", "--name-only", "#{commit}^", commit).lines.map(&:strip)
|
|
|
|
files.each do |file|
|
|
|
|
files_to_commits[file] ||= []
|
|
|
|
files_to_commits[file] << commit
|
|
|
|
tap_file = (tap.path/file).to_s
|
|
|
|
if (tap_file.start_with?("#{tap.formula_dir}/") || tap_file.start_with?("#{tap.cask_dir}/")) &&
|
|
|
|
File.extname(file) == ".rb"
|
|
|
|
next
|
|
|
|
end
|
2022-07-04 19:34:30 +02:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
odie <<~EOS
|
|
|
|
Autosquash can only squash commits that modify formula or cask files.
|
|
|
|
File: #{file}
|
|
|
|
Commit: #{commit}
|
|
|
|
EOS
|
|
|
|
end
|
|
|
|
[commit, files]
|
|
|
|
end
|
2022-08-04 23:18:19 +02:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Reset to state before cherry-picking.
|
|
|
|
safe_system "git", "-C", tap.path, "reset", "--hard", original_commit
|
2022-07-04 19:34:30 +02:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
# Iterate over every commit in the pull request series, but if we have to squash
|
|
|
|
# multiple commits into one, ensure that we skip over commits we've already squashed.
|
|
|
|
processed_commits = T.let([], T::Array[String])
|
|
|
|
commits.each do |commit|
|
|
|
|
next if processed_commits.include? commit
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
files = commits_to_files[commit]
|
|
|
|
if files.length == 1 && files_to_commits[files.first].length == 1
|
|
|
|
# If there's a 1:1 mapping of commits to files, just cherry pick and (maybe) reword.
|
|
|
|
reword_package_commit(
|
|
|
|
commit, files.first, git_repo:, reason:, verbose:, resolve:
|
|
|
|
)
|
|
|
|
processed_commits << commit
|
|
|
|
elsif files.length == 1 && files_to_commits[files.first].length > 1
|
|
|
|
# If multiple commits modify a single file, squash them down into a single commit.
|
|
|
|
file = files.first
|
|
|
|
commits = files_to_commits[file]
|
|
|
|
squash_package_commits(commits, file, git_repo:, reason:, verbose:, resolve:)
|
|
|
|
processed_commits += commits
|
|
|
|
else
|
|
|
|
# We can't split commits (yet) so just raise an error.
|
|
|
|
odie <<~EOS
|
|
|
|
Autosquash can't split commits that modify multiple files.
|
|
|
|
Commit: #{commit}
|
|
|
|
Files: #{files.join " "}
|
|
|
|
EOS
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue
|
2024-07-02 15:24:01 +01:00
|
|
|
original_head = git_repo&.head_ref
|
|
|
|
return if original_head.nil?
|
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
opoo "Autosquash encountered an error; resetting to original state at #{original_head}"
|
2024-07-02 15:24:01 +01:00
|
|
|
system "git", "-C", tap.path.to_s, "reset", "--hard", original_head
|
2024-07-01 18:07:47 +01:00
|
|
|
system "git", "-C", tap.path.to_s, "cherry-pick", "--abort" if cherry_picked
|
2024-03-21 21:31:25 -07:00
|
|
|
raise
|
|
|
|
end
|
2022-03-02 05:19:41 +00:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
private
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(user: String, repo: String, pull_request: String, path: T.any(String, Pathname)).void }
|
2024-03-21 21:31:25 -07:00
|
|
|
def cherry_pick_pr!(user, repo, pull_request, path: ".")
|
|
|
|
if args.dry_run?
|
|
|
|
puts <<~EOS
|
|
|
|
git fetch --force origin +refs/pull/#{pull_request}/head
|
|
|
|
git merge-base HEAD FETCH_HEAD
|
|
|
|
git cherry-pick --ff --allow-empty $merge_base..FETCH_HEAD
|
|
|
|
EOS
|
|
|
|
return
|
|
|
|
end
|
2020-03-31 22:11:30 +11:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
commits = GitHub.pull_request_commits(user, repo, pull_request)
|
|
|
|
safe_system "git", "-C", path, "fetch", "--quiet", "--force", "origin", commits.last
|
|
|
|
ohai "Using #{commits.count} commit#{"s" if commits.count != 1} from ##{pull_request}"
|
|
|
|
Utils::Git.cherry_pick!(path, "--ff", "--allow-empty", *commits, verbose: args.verbose?,
|
|
|
|
resolve: args.resolve?)
|
|
|
|
end
|
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(tap: Tap, original_commit: String, labels: T::Array[String]).returns(T::Boolean) }
|
2024-03-21 21:31:25 -07:00
|
|
|
def formulae_need_bottles?(tap, original_commit, labels)
|
|
|
|
return false if args.dry_run?
|
2021-04-01 16:23:39 +05:30
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
return false if labels.include?("CI-syntax-only") || labels.include?("CI-no-bottles")
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
changed_packages(tap, original_commit).any? do |f|
|
|
|
|
!f.instance_of?(Cask::Cask)
|
|
|
|
end
|
2020-09-17 16:22:36 +10:00
|
|
|
end
|
2020-03-30 00:47:38 +11:00
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(tap: Tap, original_commit: String).returns(T::Array[String]) }
|
2024-03-21 21:31:25 -07:00
|
|
|
def changed_packages(tap, original_commit)
|
|
|
|
formulae = Utils.popen_read("git", "-C", tap.path, "diff-tree",
|
|
|
|
"-r", "--name-only", "--diff-filter=AM",
|
|
|
|
original_commit, "HEAD", "--", tap.formula_dir)
|
|
|
|
.lines
|
|
|
|
.filter_map do |line|
|
|
|
|
next unless line.end_with? ".rb\n"
|
|
|
|
|
|
|
|
name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
|
|
|
|
if Homebrew::EnvConfig.disable_load_formula?
|
|
|
|
opoo "Can't check if updated bottles are necessary as HOMEBREW_DISABLE_LOAD_FORMULA is set!"
|
|
|
|
break
|
|
|
|
end
|
|
|
|
begin
|
|
|
|
Formulary.resolve(name)
|
|
|
|
rescue FormulaUnavailableError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
casks = Utils.popen_read("git", "-C", tap.path, "diff-tree",
|
|
|
|
"-r", "--name-only", "--diff-filter=AM",
|
|
|
|
original_commit, "HEAD", "--", tap.cask_dir)
|
|
|
|
.lines
|
|
|
|
.filter_map do |line|
|
|
|
|
next unless line.end_with? ".rb\n"
|
|
|
|
|
|
|
|
name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
|
|
|
|
begin
|
|
|
|
Cask::CaskLoader.load(name)
|
|
|
|
rescue Cask::CaskUnavailableError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
formulae + casks
|
2023-03-28 21:59:53 +08:00
|
|
|
end
|
|
|
|
|
2024-07-01 18:07:47 +01:00
|
|
|
sig { params(repo: String, pull_request: String).void }
|
2024-03-21 21:31:25 -07:00
|
|
|
def pr_check_conflicts(repo, pull_request)
|
|
|
|
long_build_pr_files = GitHub.issues(
|
|
|
|
repo:, state: "open", labels: "no long build conflict",
|
|
|
|
).each_with_object({}) do |long_build_pr, hash|
|
|
|
|
next unless long_build_pr.key?("pull_request")
|
2022-07-04 19:34:30 +02:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
number = long_build_pr["number"]
|
|
|
|
next if number == pull_request.to_i
|
2021-04-06 13:30:07 +01:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
GitHub.get_pull_request_changed_files(repo, number).each do |file|
|
|
|
|
key = file["filename"]
|
|
|
|
hash[key] ||= []
|
|
|
|
hash[key] << number
|
2020-06-20 21:55:49 +10:00
|
|
|
end
|
2024-03-21 21:31:25 -07:00
|
|
|
end
|
2020-06-20 21:55:49 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
return if long_build_pr_files.blank?
|
2020-04-11 19:27:34 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
this_pr_files = GitHub.get_pull_request_changed_files(repo, pull_request)
|
2020-11-22 15:14:42 +01:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
conflicts = this_pr_files.each_with_object({}) do |file, hash|
|
|
|
|
filename = file["filename"]
|
|
|
|
next unless long_build_pr_files.key?(filename)
|
2020-04-11 19:27:34 +10:00
|
|
|
|
2024-03-21 21:31:25 -07:00
|
|
|
long_build_pr_files[filename].each do |pr_number|
|
|
|
|
key = "#{repo}/pull/#{pr_number}"
|
|
|
|
hash[key] ||= []
|
|
|
|
hash[key] << filename
|
2024-01-25 11:45:20 -05:00
|
|
|
end
|
2024-01-25 10:31:22 -05:00
|
|
|
end
|
2024-03-21 21:31:25 -07:00
|
|
|
|
|
|
|
return if conflicts.blank?
|
|
|
|
|
|
|
|
# Raise an error, display the conflicting PR. For example:
|
|
|
|
# Error: You are trying to merge a pull request that conflicts with a long running build in:
|
|
|
|
# {
|
|
|
|
# "homebrew-core/pull/98809": [
|
|
|
|
# "Formula/icu4c.rb",
|
|
|
|
# "Formula/node@10.rb"
|
|
|
|
# ]
|
|
|
|
# }
|
|
|
|
odie <<~EOS
|
|
|
|
You are trying to merge a pull request that conflicts with a long running build in:
|
|
|
|
#{JSON.pretty_generate(conflicts)}
|
|
|
|
EOS
|
2020-03-30 00:47:38 +11:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|