mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

There are times where a regex isn't needed in a `strategy` block and these changes explicitly handle that situation. This allows the Symbol Proc format used in some `Sparkle` `livecheck` blocks (e.g., `strategy :sparkle, &:version`) to continue working instead of failing with a "wrong number of arguments (given 1, expected 0)" error. This error would occur because a Symbol Proc only only expects one argument (e.g., an `Item`, not an `Item` and a regex/nil). There's an argument to be made for avoiding the Symbol Proc format for `strategy` blocks but I haven't found a way of selectively disabling the Style/SymbolProc cop only for a `strategy` DSL method call. That is to say, if we don't use the Symbol Proc format, `brew style` will give a "Pass &:version as an argument to strategy instead of a block." offense. Besides that, this also replaces the `block` type signatures in livecheck strategies with `T.untyped`. Sorbet doesn't know how to handle a `Proc` with a variable number of arguments and can't be taught how (i.e., `T.any` with a `Proc` signature for each variation doesn't work). The aforementioned changes cause Sorbet to complain about there being both too many and too few arguments, so the only way to win is not to play the game. Hopefully we can restore the `block` type signatures in the future (if upstream resolves this years-old issue) but `T.untyped` seems to be our only option for now.
160 lines
5.9 KiB
Ruby
160 lines
5.9 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
require "open3"
|
|
|
|
module Homebrew
|
|
module Livecheck
|
|
module Strategy
|
|
# The {Git} strategy identifies versions of software in a Git repository
|
|
# by checking the tags using `git ls-remote --tags`.
|
|
#
|
|
# Livecheck has historically prioritized the {Git} strategy over others
|
|
# and this behavior was continued when the priority setup was created.
|
|
# This is partly related to Livecheck checking formula URLs in order of
|
|
# `head`, `stable`, and then `homepage`. The higher priority here may
|
|
# be removed (or altered) in the future if we reevaluate this particular
|
|
# behavior.
|
|
#
|
|
# This strategy does not have a default regex. Instead, it simply removes
|
|
# any non-digit text from the start of tags and parses the rest as a
|
|
# {Version}. This works for some simple situations but even one unusual
|
|
# tag can cause a bad result. It's better to provide a regex in a
|
|
# `livecheck` block, so `livecheck` only matches what we really want.
|
|
#
|
|
# @api public
|
|
class Git
|
|
extend T::Sig
|
|
|
|
# The priority of the strategy on an informal scale of 1 to 10 (from
|
|
# lowest to highest).
|
|
PRIORITY = 8
|
|
|
|
# The default regex used to naively identify versions from tags when a
|
|
# regex isn't provided.
|
|
DEFAULT_REGEX = /\D*(.+)/.freeze
|
|
|
|
# Whether the strategy can be applied to the provided URL.
|
|
#
|
|
# @param url [String] the URL to match against
|
|
# @return [Boolean]
|
|
sig { params(url: String).returns(T::Boolean) }
|
|
def self.match?(url)
|
|
(DownloadStrategyDetector.detect(url) <= GitDownloadStrategy) == true
|
|
end
|
|
|
|
# Fetches a remote Git repository's tags using `git ls-remote --tags`
|
|
# and parses the command's output. If a regex is provided, it will be
|
|
# used to filter out any tags that don't match it.
|
|
#
|
|
# @param url [String] the URL of the Git repository to check
|
|
# @param regex [Regexp] the regex to use for filtering tags
|
|
# @return [Hash]
|
|
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) }
|
|
def self.tag_info(url, regex = nil)
|
|
# Open3#capture3 is used here because we need to capture stderr
|
|
# output and handle it in an appropriate manner. Alternatives like
|
|
# SystemCommand always print errors (as well as debug output) and
|
|
# don't meet the same goals.
|
|
stdout_str, stderr_str, _status = Open3.capture3(
|
|
{ "GIT_TERMINAL_PROMPT" => "0" }, "git", "ls-remote", "--tags", url
|
|
)
|
|
|
|
tags_data = { tags: [] }
|
|
tags_data[:messages] = stderr_str.split("\n") if stderr_str.present?
|
|
return tags_data if stdout_str.blank?
|
|
|
|
# Isolate tag strings by removing leading/trailing text
|
|
stdout_str.gsub!(%r{^.*\trefs/tags/}, "")
|
|
stdout_str.gsub!("^{}", "")
|
|
|
|
tags = stdout_str.split("\n").uniq.sort
|
|
tags.select! { |t| t =~ regex } if regex
|
|
tags_data[:tags] = tags
|
|
|
|
tags_data
|
|
end
|
|
|
|
# Identify versions from tag strings using a provided regex or the
|
|
# `DEFAULT_REGEX`. The regex is expected to use a capture group around
|
|
# the version text.
|
|
#
|
|
# @param tags [Array] the tags to identify versions from
|
|
# @param regex [Regexp, nil] a regex to identify versions
|
|
# @return [Array]
|
|
sig {
|
|
params(
|
|
tags: T::Array[String],
|
|
regex: T.nilable(Regexp),
|
|
block: T.untyped,
|
|
).returns(T::Array[String])
|
|
}
|
|
def self.versions_from_tags(tags, regex = nil, &block)
|
|
if block
|
|
block_return_value = if regex.present?
|
|
yield(tags, regex)
|
|
elsif block.arity == 2
|
|
yield(tags, DEFAULT_REGEX)
|
|
else
|
|
yield(tags)
|
|
end
|
|
return Strategy.handle_block_return(block_return_value)
|
|
end
|
|
|
|
tags_only_debian = tags.all? { |tag| tag.start_with?("debian/") }
|
|
|
|
tags.map do |tag|
|
|
# Skip tag if it has a 'debian/' prefix and upstream does not do
|
|
# only 'debian/' prefixed tags
|
|
next if tag =~ %r{^debian/} && !tags_only_debian
|
|
|
|
if regex
|
|
# Use the first capture group (the version)
|
|
tag.scan(regex).first&.first
|
|
else
|
|
# Remove non-digits from the start of the tag and use that as the
|
|
# version text
|
|
tag[DEFAULT_REGEX, 1]
|
|
end
|
|
end.compact.uniq
|
|
end
|
|
|
|
# Checks the Git tags for new versions. When a regex isn't provided,
|
|
# this strategy simply removes non-digits from the start of tag
|
|
# strings and parses the remaining text as a {Version}.
|
|
#
|
|
# @param url [String] the URL of the Git repository to check
|
|
# @param regex [Regexp, nil] a regex used for matching versions
|
|
# @return [Hash]
|
|
sig {
|
|
params(
|
|
url: String,
|
|
regex: T.nilable(Regexp),
|
|
_unused: T.nilable(T::Hash[Symbol, T.untyped]),
|
|
block: T.untyped,
|
|
).returns(T::Hash[Symbol, T.untyped])
|
|
}
|
|
def self.find_versions(url:, regex: nil, **_unused, &block)
|
|
match_data = { matches: {}, regex: regex, url: url }
|
|
|
|
tags_data = tag_info(url, regex)
|
|
tags = tags_data[:tags]
|
|
|
|
if tags_data.key?(:messages)
|
|
match_data[:messages] = tags_data[:messages]
|
|
return match_data if tags.blank?
|
|
end
|
|
|
|
versions_from_tags(tags, regex, &block).each do |version_text|
|
|
match_data[:matches][version_text] = Version.new(version_text)
|
|
rescue TypeError
|
|
next
|
|
end
|
|
|
|
match_data
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|