brew/Library/Homebrew/dev-cmd/bump-cask-pr.rb

328 lines
13 KiB
Ruby
Raw Normal View History

# typed: strict
2020-09-04 16:58:31 -07:00
# frozen_string_literal: true
2024-03-18 15:56:38 -07:00
require "abstract_command"
2023-07-25 06:38:00 -04:00
require "bump_version_parser"
2020-09-04 16:58:31 -07:00
require "cask"
require "cask/download"
2020-09-04 16:58:31 -07:00
require "cli/parser"
require "utils/tar"
module Homebrew
2024-03-18 15:56:38 -07:00
module DevCmd
class BumpCaskPr < AbstractCommand
cmd_args do
description <<~EOS
Create a pull request to update <cask> with a new version.
A best effort to determine the <SHA-256> will be made if the value is not
supplied by the user.
EOS
switch "-n", "--dry-run",
description: "Print what would be done rather than doing it."
switch "--write-only",
description: "Make the expected file modifications without taking any Git actions."
switch "--commit",
depends_on: "--write-only",
description: "When passed with `--write-only`, generate a new commit after writing changes " \
"to the cask file."
switch "--no-audit",
description: "Don't run `brew audit` before opening the PR."
switch "--online",
hidden: true
switch "--no-style",
description: "Don't run `brew style --fix` before opening the PR."
switch "--no-browse",
description: "Print the pull request URL instead of opening in a browser."
switch "--no-fork",
description: "Don't try to fork the repository."
flag "--version=",
description: "Specify the new <version> for the cask."
flag "--version-arm=",
description: "Specify the new cask <version> for the ARM architecture."
flag "--version-intel=",
description: "Specify the new cask <version> for the Intel architecture."
flag "--message=",
description: "Prepend <message> to the default pull request message."
flag "--url=",
description: "Specify the <URL> for the new download."
flag "--sha256=",
description: "Specify the <SHA-256> checksum of the new download."
flag "--fork-org=",
description: "Use the specified GitHub organization for forking."
switch "-f", "--force",
hidden: true
conflicts "--dry-run", "--write"
conflicts "--no-audit", "--online"
conflicts "--version=", "--version-arm="
conflicts "--version=", "--version-intel="
named_args :cask, number: 1, without_api: true
end
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
sig { override.void }
def run
odeprecated "brew bump-cask-pr --online" if args.online?
odisabled "brew bump-cask-pr --force" if args.force?
2024-03-18 15:56:38 -07:00
# This will be run by `brew audit` or `brew style` later so run it first to
# not start spamming during normal output.
gem_groups = []
gem_groups << "style" if !args.no_audit? || !args.no_style?
gem_groups << "audit" unless args.no_audit?
Homebrew.install_bundler_gems!(groups: gem_groups) unless gem_groups.empty?
2024-03-18 15:56:38 -07:00
# As this command is simplifying user-run commands then let's just use a
# user path, too.
ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
# Use the user's browser, too.
ENV["BROWSER"] = EnvConfig.browser
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
cask = args.named.to_casks.first
2021-01-08 11:42:37 -08:00
2024-03-18 15:56:38 -07:00
odie "This cask is not in a tap!" if cask.tap.blank?
odie "This cask's tap is not a Git repository!" unless cask.tap.git?
2021-01-08 11:42:37 -08:00
2024-03-18 15:56:38 -07:00
odie <<~EOS unless cask.tap.allow_bump?(cask.token)
Whoops, the #{cask.token} cask has its version update
pull requests automatically opened by BrewTestBot!
We'd still love your contributions, though, so try another one
that's not in the autobump list:
#{Formatter.url("#{cask.tap.remote}/blob/master/.github/autobump.txt")}
EOS
2024-03-18 15:56:38 -07:00
new_version = BumpVersionParser.new(
general: args.version,
intel: args.version_intel,
arm: args.version_arm,
)
2024-03-18 15:56:38 -07:00
new_hash = unless (new_hash = args.sha256).nil?
raise UsageError, "`--sha256` must not be empty." if new_hash.blank?
2024-03-18 15:56:38 -07:00
["no_check", ":no_check"].include?(new_hash) ? :no_check : new_hash
end
2024-03-18 15:56:38 -07:00
new_base_url = unless (new_base_url = args.url).nil?
raise UsageError, "`--url` must not be empty." if new_base_url.blank?
2024-03-18 15:56:38 -07:00
begin
URI(new_base_url)
rescue URI::InvalidURIError
raise UsageError, "`--url` is not valid."
end
end
2024-03-18 15:56:38 -07:00
if new_version.blank? && new_base_url.nil? && new_hash.nil?
raise UsageError, "No `--version`, `--url` or `--sha256` argument specified!"
end
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
check_pull_requests(cask, new_version:)
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
replacement_pairs ||= []
branch_name = "bump-#{cask.token}"
commit_message = nil
2024-03-18 15:56:38 -07:00
old_contents = File.read(cask.sourcefile_path)
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
if new_base_url
commit_message ||= "#{cask.token}: update URL"
2024-03-18 15:56:38 -07:00
m = /^ +url "(.+?)"\n/m.match(old_contents)
odie "Could not find old URL in cask!" if m.nil?
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
old_base_url = m.captures.fetch(0)
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
replacement_pairs << [
/#{Regexp.escape(old_base_url)}/,
new_base_url.to_s,
]
end
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
if new_version.present?
# For simplicity, our naming defers to the arm version if we multiple architectures are specified
branch_version = new_version.arm || new_version.general
if branch_version.is_a?(Cask::DSL::Version)
commit_version = shortened_version(branch_version, cask:)
branch_name = "bump-#{cask.token}-#{branch_version.tr(",:", "-")}"
commit_message ||= "#{cask.token} #{commit_version}"
end
replacement_pairs = replace_version_and_checksum(cask, new_hash, new_version, replacement_pairs)
end
# Now that we have all replacement pairs, we will replace them further down
commit_message ||= "#{cask.token}: update checksum" if new_hash
# Remove nested arrays where elements are identical
replacement_pairs = replacement_pairs.reject { |pair| pair[0] == pair[1] }.uniq.compact
Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs,
read_only_run: args.dry_run?,
silent: args.quiet?)
run_cask_audit(cask, old_contents)
run_cask_style(cask, old_contents)
pr_info = {
branch_name:,
commit_message:,
old_contents:,
pr_message: "Created with `brew bump-cask-pr`.",
sourcefile_path: cask.sourcefile_path,
tap: cask.tap,
}
GitHub.create_bump_pr(pr_info, args:)
end
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
private
2024-03-18 15:56:38 -07:00
sig { params(version: Cask::DSL::Version, cask: Cask::Cask).returns(Cask::DSL::Version) }
def shortened_version(version, cask:)
if version.before_comma == cask.version.before_comma
version
else
version.before_comma
end
end
2024-03-18 15:56:38 -07:00
sig {
params(
cask: Cask::Cask,
new_hash: T.any(NilClass, String, Symbol),
new_version: BumpVersionParser,
replacement_pairs: T::Array[[T.any(Regexp, String), T.any(Regexp, String)]],
).returns(T::Array[[T.any(Regexp, String), T.any(Regexp, String)]])
}
def replace_version_and_checksum(cask, new_hash, new_version, replacement_pairs)
# When blocks are absent, arch is not relevant. For consistency, we simulate the arm architecture.
arch_options = cask.on_system_blocks_exist? ? OnSystem::ARCH_OPTIONS : [:arm]
arch_options.each do |arch|
SimulateSystem.with(arch:) do
old_cask = Cask::CaskLoader.load(cask.sourcefile_path)
old_version = old_cask.version
bump_version = new_version.send(arch) || new_version.general
old_version_regex = old_version.latest? ? ":latest" : %Q(["']#{Regexp.escape(old_version.to_s)}["'])
replacement_pairs << [/version\s+#{old_version_regex}/m,
"version #{bump_version.latest? ? ":latest" : %Q("#{bump_version}")}"]
# We are replacing our version here so we can get the new hash
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs.uniq.compact,
read_only_run: true,
silent: true)
tmp_cask = Cask::CaskLoader.load(tmp_contents)
old_hash = tmp_cask.sha256
if tmp_cask.version.latest? || new_hash == :no_check
opoo "Ignoring specified `--sha256=` argument." if new_hash.is_a?(String)
replacement_pairs << [/"#{old_hash}"/, ":no_check"] if old_hash != :no_check
elsif old_hash == :no_check && new_hash != :no_check
replacement_pairs << [":no_check", "\"#{new_hash}\""] if new_hash.is_a?(String)
elsif new_hash && !cask.on_system_blocks_exist? && cask.languages.empty?
replacement_pairs << [old_hash.to_s, new_hash.to_s]
elsif old_hash != :no_check
opoo "Multiple checksum replacements required; ignoring specified `--sha256` argument." if new_hash
languages = if cask.languages.empty?
[nil]
else
cask.languages
end
languages.each do |language|
new_cask = Cask::CaskLoader.load(tmp_contents)
new_cask.config = if language.blank?
tmp_cask.config
else
tmp_cask.config.merge(Cask::Config.new(explicit: { languages: [language] }))
end
download = Cask::Download.new(new_cask, quarantine: true).fetch(verify_download_integrity: false)
Utils::Tar.validate_file(download)
if new_cask.sha256.to_s != download.sha256
replacement_pairs << [new_cask.sha256.to_s,
download.sha256]
end
end
end
end
end
2024-03-18 15:56:38 -07:00
replacement_pairs
end
2024-03-18 15:56:38 -07:00
sig { params(cask: Cask::Cask, new_version: BumpVersionParser).void }
def check_pull_requests(cask, new_version:)
tap_remote_repo = cask.tap.full_name || cask.tap.remote_repo
GitHub.check_for_duplicate_pull_requests(cask.token, tap_remote_repo,
state: "open",
version: nil,
file: cask.sourcefile_path.relative_path_from(cask.tap.path).to_s,
quiet: args.quiet?)
# if we haven't already found open requests, try for an exact match across closed requests
new_version.instance_variables.each do |version_type|
version = new_version.instance_variable_get(version_type)
next if version.blank?
GitHub.check_for_duplicate_pull_requests(
cask.token,
tap_remote_repo,
state: "closed",
version: shortened_version(version, cask:),
file: cask.sourcefile_path.relative_path_from(cask.tap.path).to_s,
quiet: args.quiet?,
)
end
end
sig { params(cask: Cask::Cask, old_contents: String).void }
def run_cask_audit(cask, old_contents)
if args.dry_run?
if args.no_audit?
ohai "Skipping `brew audit`"
else
ohai "brew audit --cask --online #{cask.full_name}"
end
return
end
failed_audit = false
if args.no_audit?
ohai "Skipping `brew audit`"
else
system HOMEBREW_BREW_FILE, "audit", "--cask", "--online", cask.full_name
failed_audit = !$CHILD_STATUS.success?
end
return unless failed_audit
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
cask.sourcefile_path.atomic_write(old_contents)
odie "`brew audit` failed!"
2020-09-04 16:58:31 -07:00
end
2024-03-18 15:56:38 -07:00
sig { params(cask: Cask::Cask, old_contents: String).void }
def run_cask_style(cask, old_contents)
if args.dry_run?
if args.no_style?
ohai "Skipping `brew style --fix`"
else
ohai "brew style --fix #{cask.sourcefile_path.basename}"
end
return
end
failed_style = false
if args.no_style?
ohai "Skipping `brew style --fix`"
else
system HOMEBREW_BREW_FILE, "style", "--fix", cask.sourcefile_path
failed_style = !$CHILD_STATUS.success?
end
return unless failed_style
2020-09-04 16:58:31 -07:00
2024-03-18 15:56:38 -07:00
cask.sourcefile_path.atomic_write(old_contents)
odie "`brew style --fix` failed!"
2020-09-04 16:58:31 -07:00
end
end
end
end