diff --git a/Library/Homebrew/dev-cmd/bump-formula-pr.rb b/Library/Homebrew/dev-cmd/bump-formula-pr.rb index a718d92077..0744334108 100644 --- a/Library/Homebrew/dev-cmd/bump-formula-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-formula-pr.rb @@ -314,7 +314,7 @@ module Homebrew new_formula_version = formula_version(formula, requested_spec, new_contents) - check_for_duplicate_pull_requests(formula, tap_full_name, new_formula_version.to_s) + GitHub.check_for_duplicate_pull_requests(formula, tap_full_name, new_formula_version.to_s) if !new_mirrors && !formula_spec.mirrors.empty? if args.force? @@ -469,41 +469,6 @@ module Homebrew end end - def fetch_pull_requests(query, tap_full_name, state: nil) - GitHub.issues_for_formula(query, tap_full_name: tap_full_name, state: state).select do |pr| - pr["html_url"].include?("/pull/") && - /(^|\s)#{Regexp.quote(query)}(:|\s|$)/i =~ pr["title"] - end - rescue GitHub::RateLimitExceededError => e - opoo e.message - [] - end - - def check_for_duplicate_pull_requests(formula, tap_full_name, version) - # check for open requests - pull_requests = fetch_pull_requests(formula.name, tap_full_name, state: "open") - - # if we haven't already found open requests, try for an exact match across all requests - pull_requests = fetch_pull_requests("#{formula.name} #{version}", tap_full_name) if pull_requests.blank? - return if pull_requests.blank? - - duplicates_message = <<~EOS - These pull requests may be duplicates: - #{pull_requests.map { |pr| "#{pr["title"]} #{pr["html_url"]}" }.join("\n")} - EOS - error_message = "Duplicate PRs should not be opened. Use --force to override this error." - if args.force? && !args.quiet? - opoo duplicates_message - elsif !args.force? && args.quiet? - odie error_message - elsif !args.force? - odie <<~EOS - #{duplicates_message.chomp} - #{error_message} - EOS - end - end - def alias_update_pair(formula, new_formula_version) versioned_alias = formula.aliases.grep(/^.*@\d+(\.\d+)?$/).first return if versioned_alias.nil? diff --git a/Library/Homebrew/dev-cmd/bump.rb b/Library/Homebrew/dev-cmd/bump.rb new file mode 100644 index 0000000000..c8416b1ead --- /dev/null +++ b/Library/Homebrew/dev-cmd/bump.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require "cli/parser" + +module Homebrew + module_function + + def bump_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `bump` + + Display out-of-date brew formulae, the latest version available, and whether a pull request has been opened. + EOS + switch :verbose + switch :debug + end + end + + def bump + bump_args.parse + + outdated_repology_packages = Repology.parse_api_response + outdated_packages = validate_and_format_packages(outdated_repology_packages) + + display(outdated_packages) + end + + def validate_and_format_packages(outdated_repology_packages) + ohai "Verifying outdated repology packages as Homebrew formulae" + + packages = {} + outdated_repology_packages.each do |_name, repositories| + # identify homebrew repo + repology_homebrew_repo = repositories.find do |repo| + repo["repo"] == "homebrew" + end + + next if repology_homebrew_repo.blank? + + latest_version = repositories.find { |repo| repo["status"] == "newest" }["version"] + + packages[repology_homebrew_repo["srcname"]] = format_package(repology_homebrew_repo["srcname"], latest_version) + end + packages + end + + def format_package(package_name, latest_version) + formula = get_formula_details(package_name) + + return if formula.blank? + + tap_full_name = formula.tap&.full_name + current_version = current_formula_version(formula) + livecheck_response = livecheck_formula(package_name) + pull_requests = GitHub.check_for_duplicate_pull_requests(formula, tap_full_name, latest_version, true) + + { + repology_latest_version: latest_version, + current_formula_version: current_version.to_s, + livecheck_latest_version: livecheck_response[:livecheck_version], + open_pull_requests: pull_requests, + } + end + + def get_formula_details(formula_name) + Formula[formula_name] + rescue + nil + end + + def current_formula_version(formula) + formula.version.to_s + end + + def livecheck_formula(formula) + ohai "Checking livecheck formula: #{formula}" if Homebrew.args.verbose? + + response = Utils.popen_read(HOMEBREW_BREW_FILE, "livecheck", formula, "--quiet").chomp + + parse_livecheck_response(response) + end + + def parse_livecheck_response(response) + output = response.delete(" ").split(/:|==>/) + + # e.g. ["openclonk", "7.0", "8.1"] + package_name, brew_version, latest_version = output + + { + name: package_name, + formula_version: brew_version, + livecheck_version: latest_version, + } + end + + def display(outdated_packages) + ohai "Outdated formulae\n" + + outdated_packages.each do |formula, package_details| + ohai formula + puts "Current formula version: #{package_details[:current_formula_version]}" + puts "Latest repology version: #{package_details[:repology_latest_version]}" + puts "Latest livecheck version: #{package_details[:livecheck_latest_version]}" + puts "Open pull requests: #{package_details[:open_pull_requests]}" + end + end +end diff --git a/Library/Homebrew/utils.rb b/Library/Homebrew/utils.rb index 254ac11ea1..d4307c5a30 100644 --- a/Library/Homebrew/utils.rb +++ b/Library/Homebrew/utils.rb @@ -10,8 +10,10 @@ require "utils/github" require "utils/inreplace" require "utils/link" require "utils/popen" +require "utils/repology" require "utils/svn" require "utils/tty" +require "utils/repology" require "tap_constants" require "time" diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index 4ac886a188..f40dba7fd3 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -346,6 +346,43 @@ module GitHub prs.each { |i| puts "#{i["title"]} (#{i["html_url"]})" } end + def fetch_pull_requests(query, tap_full_name, state: nil) + issues_for_formula(query, tap_full_name: tap_full_name, state: state).select do |pr| + pr["html_url"].include?("/pull/") && + /(^|\s)#{Regexp.quote(query)}(:|\s|$)/i =~ pr["title"] + end + rescue GitHub::RateLimitExceededError => e + opoo e.message + [] + end + + def check_for_duplicate_pull_requests(formula, tap_full_name, version, fetch_pr = false) + # check for open requests + pull_requests = fetch_pull_requests(formula.name, tap_full_name, state: "open") + + # if we haven't already found open requests, try for an exact match across all requests + pull_requests = fetch_pull_requests("#{formula.name} #{version}", tap_full_name) if pull_requests.blank? + return if pull_requests.blank? + + return pull_requests.map { |pr| { title: pr["title"], url: pr["html_url"] } } if fetch_pr + + duplicates_message = <<~EOS + These pull requests may be duplicates: + #{pull_requests.map { |pr| "#{pr["title"]} #{pr["html_url"]}" }.join("\n")} + EOS + error_message = "Duplicate PRs should not be opened. Use --force to override this error." + if Homebrew.args.force? && !Homebrew.args.quiet? + opoo duplicates_message + elsif !Homebrew.args.force? && Homebrew.args.quiet? + odie error_message + elsif !Homebrew.args.force? + odie <<~EOS + #{duplicates_message.chomp} + #{error_message} + EOS + end + end + def create_fork(repo) url = "#{API_URL}/repos/#{repo}/forks" data = {} diff --git a/Library/Homebrew/utils/repology.rb b/Library/Homebrew/utils/repology.rb new file mode 100644 index 0000000000..877c6eeef1 --- /dev/null +++ b/Library/Homebrew/utils/repology.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "utils/curl" + +module Repology + module_function + + MAX_PAGINATION = 15 + + def query_api(last_package_in_response = "") + last_package_in_response += "/" if last_package_in_response.present? + url = "https://repology.org/api/v1/projects/#{last_package_in_response}?inrepo=homebrew&outdated=1" + + output, _errors, _status = curl_output(url.to_s) + JSON.parse(output) + end + + def parse_api_response + ohai "Querying outdated packages from Repology" + + outdated_packages = query_api + last_package_index = outdated_packages.size - 1 + response_size = outdated_packages.size + page_no = 1 + + while response_size > 1 && page_no <= MAX_PAGINATION + odebug "Paginating Repology API page: #{page_no}" + + last_package_in_response = outdated_packages.keys[last_package_index] + response = query_api(last_package_in_response) + + response_size = response.size + outdated_packages.merge!(response) + last_package_index = outdated_packages.size - 1 + page_no += 1 + end + + ohai "#{outdated_packages.size} outdated #{"package".pluralize(outdated_packages.size)} identified" + + outdated_packages + end +end diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index 3beacf0b09..efdf978359 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -12,6 +12,7 @@ abv analytics audit bottle +bump bump-formula-pr bump-revision cask diff --git a/docs/Manpage.md b/docs/Manpage.md index 2aa5bc9ed3..7579dca506 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -696,6 +696,11 @@ at its original value, while `--no-rebuild` will remove it. * `--root-url`: Use the specified *`URL`* as the root of the bottle's URL instead of Homebrew's default. +### `bump` + +Display out-of-date brew formulae, the latest version available, and whether a +pull request has been opened. + ### `bump-formula-pr` [*`options`*] [*`formula`*] Create a pull request to update *`formula`* with a new URL or a new tag. diff --git a/manpages/brew.1 b/manpages/brew.1 index 525eeddfd8..556e11ca3e 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -906,6 +906,9 @@ When passed with \fB\-\-write\fR, a new commit will not generated after writing \fB\-\-root\-url\fR Use the specified \fIURL\fR as the root of the bottle\'s URL instead of Homebrew\'s default\. . +.SS "\fBbump\fR" +Display out\-of\-date brew formulae, the latest version available, and whether a pull request has been opened\. +. .SS "\fBbump\-formula\-pr\fR [\fIoptions\fR] [\fIformula\fR]" Create a pull request to update \fIformula\fR with a new URL or a new tag\. .