diff --git a/Library/Homebrew/dev-cmd/bump-cask-pr.rb b/Library/Homebrew/dev-cmd/bump-cask-pr.rb new file mode 100644 index 0000000000000000000000000000000000000000..20613d12c43379a91b0905a1b00f46b1944b1dc0 --- /dev/null +++ b/Library/Homebrew/dev-cmd/bump-cask-pr.rb @@ -0,0 +1,237 @@ +# frozen_string_literal: true + +require "cask" +require "cli/parser" +require "utils/tar" + +module Homebrew + module_function + + def bump_cask_pr_args + Homebrew::CLI::Parser.new do + usage_banner <<~EOS + `bump-cask-pr` [<options>] [<cask>] + + 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", + description: "Make the expected file modifications without taking any Git actions." + switch "--commit", + depends_on: "--write", + description: "When passed with `--write`, generate a new commit after writing changes "\ + "to the cask file." + switch "--no-audit", + description: "Don't run `brew cask audit` before opening the PR." + switch "--no-style", + description: "Don't run `brew cask 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 "--message=", + description: "Append <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." + switch "-f", "--force", + description: "Ignore duplicate open PRs." + + conflicts "--dry-run", "--write" + named 1 + end + end + + def bump_cask_pr + args = bump_cask_pr_args.parse + + # As this command is simplifying user-run commands then let's just use a + # user path, too. + ENV["PATH"] = ENV["HOMEBREW_PATH"] + + # Use the user's browser, too. + ENV["BROWSER"] = Homebrew::EnvConfig.browser + + cask = args.named.to_casks.first + new_version = args.version + new_base_url = args.url + new_hash = args.sha256 + + old_version = cask.version + old_hash = cask.sha256 + + tap_full_name = cask.tap&.full_name + origin_branch = Utils::Git.origin_branch(cask.tap.path) if cask.tap + origin_branch ||= "origin/master" + previous_branch = "-" + + check_open_pull_requests(cask, tap_full_name, args: args) + + odie "#{cask}: no --version= argument specified!" unless new_version + + check_closed_pull_requests(cask, tap_full_name, version: new_version, args: args) + + if Version.new(new_version) < Version.new(old_version) + odie <<~EOS + You need to bump this cask manually since changing the + version from #{old_version} to #{new_version} would be a downgrade. + EOS + elsif new_version == old_version + odie <<~EOS + You need to bump this cask manually since the new version + and old version are both #{new_version}. + EOS + end + + old_contents = File.read(cask.sourcefile_path) + + replacement_pairs = [ + [ + old_version, + new_version, + ], + ] + + if new_base_url.present? + m = /^ +url "(.+?)"\n/m.match(old_contents) + odie "Could not find old URL in cask!" if m.nil? + + old_base_url = m.captures.first + + replacement_pairs << [ + /#{Regexp.escape(old_base_url)}/, + new_base_url, + ] + end + + if new_hash.nil? || cask.languages.present? + 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) + tmp_url = tmp_cask.url.to_s + + if new_hash.nil? + resource_path = fetch_resource(cask, new_version, tmp_url) + Utils::Tar.validate_file(resource_path) + new_hash = resource_path.sha256 + end + + cask.languages.each do |language| + next if language == cask.language + + tmp_cask.config.languages = [language] + + lang_cask = Cask::CaskLoader.load(tmp_contents) + lang_url = lang_cask.url.to_s + lang_old_hash = lang_cask.sha256 + + resource_path = fetch_resource(cask, new_version, lang_url) + Utils::Tar.validate_file(resource_path) + lang_new_hash = resource_path.sha256 + + replacement_pairs << [ + lang_old_hash, + lang_new_hash, + ] + end + end + + replacement_pairs << [ + old_hash, + new_hash, + ] + + Utils::Inreplace.inreplace_pairs(cask.sourcefile_path, + replacement_pairs.uniq.compact, + read_only_run: args.dry_run?, + silent: args.quiet?) + + run_cask_audit(cask, old_contents, args: args) + run_cask_style(cask, old_contents, args: args) + + pr_info = { + sourcefile_path: cask.sourcefile_path, + old_contents: old_contents, + origin_branch: origin_branch, + branch_name: "bump-#{cask.token}-#{new_version}", + commit_message: "Update #{cask.token} from #{old_version} to #{new_version}", + previous_branch: previous_branch, + tap: cask.tap, + tap_full_name: tap_full_name, + pr_message: "Created with `brew bump-cask-pr`.", + } + GitHub.create_bump_pr(pr_info, args: args) + end + + def fetch_resource(cask, new_version, url, **specs) + resource = Resource.new + resource.url(url, specs) + resource.owner = Resource.new(cask.token) + resource.version = new_version + resource.fetch + end + + def check_open_pull_requests(cask, tap_full_name, args:) + GitHub.check_for_duplicate_pull_requests(cask.token, tap_full_name, state: "open", args: args) + end + + def check_closed_pull_requests(cask, tap_full_name, version:, args:) + # if we haven't already found open requests, try for an exact match across closed requests + pr_title = "Update #{cask.token} from #{cask.version} to #{version}" + GitHub.check_for_duplicate_pull_requests(pr_title, tap_full_name, state: "closed", args: args) + end + + def run_cask_audit(cask, old_contents, args:) + if args.dry_run? + if args.no_audit? + ohai "Skipping `brew cask audit`" + else + ohai "brew cask audit #{cask.sourcefile_path.basename}" + end + return + end + failed_audit = false + if args.no_audit? + ohai "Skipping `brew cask audit`" + else + system HOMEBREW_BREW_FILE, "cask", "audit", cask.sourcefile_path + failed_audit = !$CHILD_STATUS.success? + end + return unless failed_audit + + cask.sourcefile_path.atomic_write(old_contents) + odie "`brew cask audit` failed!" + end + + def run_cask_style(cask, old_contents, args:) + if args.dry_run? + if args.no_style? + ohai "Skipping `brew cask style --fix`" + else + ohai "brew cask style --fix #{cask.sourcefile_path.basename}" + end + return + end + failed_style = false + if args.no_style? + ohai "Skipping `brew cask style --fix`" + else + system HOMEBREW_BREW_FILE, "cask", "style", "--fix", cask.sourcefile_path + failed_style = !$CHILD_STATUS.success? + end + return unless failed_style + + cask.sourcefile_path.atomic_write(old_contents) + odie "`brew cask style --fix` failed!" + end +end diff --git a/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb b/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..69552cfd785ee78fc62e6af247417fdb5487f73e --- /dev/null +++ b/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "cmd/shared_examples/args_parse" + +describe "Homebrew.bump_cask_pr_args" do + it_behaves_like "parseable arguments" +end diff --git a/Library/Homebrew/utils/git.rb b/Library/Homebrew/utils/git.rb index 56727092dd78446b144a0e441fc8b5a9718cd849..4bb3eb2df57b793969c73f288516f49216bc5e4d 100644 --- a/Library/Homebrew/utils/git.rb +++ b/Library/Homebrew/utils/git.rb @@ -119,5 +119,10 @@ module Utils ENV["GIT_AUTHOR_EMAIL"] = Homebrew::EnvConfig.git_email if author ENV["GIT_COMMITTER_EMAIL"] = Homebrew::EnvConfig.git_email if committer end + + def origin_branch(repo) + Utils.popen_read("git", "-C", repo, "symbolic-ref", "-q", "--short", + "refs/remotes/origin/HEAD").chomp.presence + end end end diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index 3d2c08bc680dd8dcd4ff984b81d56bde85f8d19c..cf63a940c6f57471dc18b5d19f978eec037d748c 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -14,6 +14,7 @@ analytics audit bottle bump +bump-cask-pr bump-formula-pr bump-revision cask diff --git a/docs/Manpage.md b/docs/Manpage.md index 0e09b636ded752bb859d9dc129d3014d542a891f..ef0263e06c491978bfc61cff48f0dbd3ff8c11b7 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -819,6 +819,38 @@ Also displays whether a pull request has been opened with the URL. * `--limit`: Limit number of package results returned. +### `bump-cask-pr` [*`options`*] [*`cask`*] + +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. + +* `-n`, `--dry-run`: + Print what would be done rather than doing it. +* `--write`: + Make the expected file modifications without taking any Git actions. +* `--commit`: + When passed with `--write`, generate a new commit after writing changes to the cask file. +* `--no-audit`: + Don't run `brew cask audit` before opening the PR. +* `--no-style`: + Don't run `brew cask style --fix` before opening the PR. +* `--no-browse`: + Print the pull request URL instead of opening in a browser. +* `--no-fork`: + Don't try to fork the repository. +* `--version`: + Specify the new *`version`* for the cask. +* `--message`: + Append *`message`* to the default pull request message. +* `--url`: + Specify the *`URL`* for the new download. +* `--sha256`: + Specify the *`SHA-256`* checksum of the new download. +* `-f`, `--force`: + Ignore duplicate open PRs. + ### `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 fcd4e4d63f93cab0da7070fea6cb8eb832ccc685..acdb80a889c1ff61d591d0c37db8b8eeca879f62 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -1134,6 +1134,60 @@ Display out\-of\-date brew formulae and the latest version available\. Also disp \fB\-\-limit\fR Limit number of package results returned\. . +.SS "\fBbump\-cask\-pr\fR [\fIoptions\fR] [\fIcask\fR]" +Create a pull request to update \fIcask\fR with a new version\. +. +.P +A best effort to determine the \fISHA\-256\fR will be made if the value is not supplied by the user\. +. +.TP +\fB\-n\fR, \fB\-\-dry\-run\fR +Print what would be done rather than doing it\. +. +.TP +\fB\-\-write\fR +Make the expected file modifications without taking any Git actions\. +. +.TP +\fB\-\-commit\fR +When passed with \fB\-\-write\fR, generate a new commit after writing changes to the cask file\. +. +.TP +\fB\-\-no\-audit\fR +Don\'t run \fBbrew cask audit\fR before opening the PR\. +. +.TP +\fB\-\-no\-style\fR +Don\'t run \fBbrew cask style \-\-fix\fR before opening the PR\. +. +.TP +\fB\-\-no\-browse\fR +Print the pull request URL instead of opening in a browser\. +. +.TP +\fB\-\-no\-fork\fR +Don\'t try to fork the repository\. +. +.TP +\fB\-\-version\fR +Specify the new \fIversion\fR for the cask\. +. +.TP +\fB\-\-message\fR +Append \fImessage\fR to the default pull request message\. +. +.TP +\fB\-\-url\fR +Specify the \fIURL\fR for the new download\. +. +.TP +\fB\-\-sha256\fR +Specify the \fISHA\-256\fR checksum of the new download\. +. +.TP +\fB\-f\fR, \fB\-\-force\fR +Ignore duplicate open PRs\. +. .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\. .