Merge pull request #5740 from reitermarkus/automerge
Add `brew cask automerge` command.
This commit is contained in:
commit
706c911ebe
@ -9,6 +9,7 @@ require "cask/cmd/options"
|
|||||||
|
|
||||||
require "cask/cmd/abstract_command"
|
require "cask/cmd/abstract_command"
|
||||||
require "cask/cmd/audit"
|
require "cask/cmd/audit"
|
||||||
|
require "cask/cmd/automerge"
|
||||||
require "cask/cmd/cat"
|
require "cask/cmd/cat"
|
||||||
require "cask/cmd/create"
|
require "cask/cmd/create"
|
||||||
require "cask/cmd/doctor"
|
require "cask/cmd/doctor"
|
||||||
|
124
Library/Homebrew/cask/cmd/automerge.rb
Executable file
124
Library/Homebrew/cask/cmd/automerge.rb
Executable file
@ -0,0 +1,124 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "cask/cmd/abstract_internal_command"
|
||||||
|
require "tap"
|
||||||
|
require "utils/formatter"
|
||||||
|
require "utils/github"
|
||||||
|
|
||||||
|
module Cask
|
||||||
|
class Cmd
|
||||||
|
class Automerge < AbstractInternalCommand
|
||||||
|
OFFICIAL_CASK_TAPS = [
|
||||||
|
"homebrew/cask",
|
||||||
|
"homebrew/cask-drivers",
|
||||||
|
"homebrew/cask-eid",
|
||||||
|
"homebrew/cask-fonts",
|
||||||
|
"homebrew/cask-versions",
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def run
|
||||||
|
taps = OFFICIAL_CASK_TAPS.map(&Tap.public_method(:fetch))
|
||||||
|
|
||||||
|
access = taps.all? { |tap| GitHub.write_access?(tap.full_name) }
|
||||||
|
raise "This command may only be run by Homebrew maintainers." unless access
|
||||||
|
|
||||||
|
Homebrew.install_gem! "git_diff"
|
||||||
|
require "git_diff"
|
||||||
|
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
taps.each do |tap|
|
||||||
|
open_pull_requests = GitHub.pull_requests(tap.full_name, state: :open, base: "master")
|
||||||
|
|
||||||
|
open_pull_requests.each do |pr|
|
||||||
|
next unless passed_ci(pr)
|
||||||
|
next unless check_diff(pr)
|
||||||
|
|
||||||
|
number = pr["number"]
|
||||||
|
sha = pr.dig("head", "sha")
|
||||||
|
|
||||||
|
print "#{Formatter.url(pr["html_url"])} "
|
||||||
|
|
||||||
|
retried = false
|
||||||
|
|
||||||
|
begin
|
||||||
|
GitHub.merge_pull_request(
|
||||||
|
tap.full_name,
|
||||||
|
number: number, sha: sha,
|
||||||
|
merge_method: :squash,
|
||||||
|
commit_message: "Squashed and auto-merged via `brew cask automerge`."
|
||||||
|
)
|
||||||
|
puts "#{Tty.bold}#{Formatter.success("✔")}#{Tty.reset}"
|
||||||
|
rescue
|
||||||
|
unless retried
|
||||||
|
retried = true
|
||||||
|
sleep 5
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Tty.bold}#{Formatter.error("✘")}#{Tty.reset}"
|
||||||
|
failed << pr["html_url"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return if failed.empty?
|
||||||
|
$stderr.puts
|
||||||
|
raise CaskError, "Failed merging the following PRs:\n#{failed.join("\n")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def passed_ci(pr)
|
||||||
|
statuses = GitHub.open_api(pr["statuses_url"])
|
||||||
|
|
||||||
|
latest_pr_status = statuses.select { |status| status["context"] == "continuous-integration/travis-ci/pr" }
|
||||||
|
.max_by { |status| Time.parse(status["updated_at"]) }
|
||||||
|
|
||||||
|
latest_pr_status&.fetch("state") == "success"
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_diff(pr)
|
||||||
|
diff_url = pr["diff_url"]
|
||||||
|
|
||||||
|
output, _, status = curl_output("--location", diff_url)
|
||||||
|
|
||||||
|
return false unless status.success?
|
||||||
|
|
||||||
|
diff = GitDiff.from_string(output)
|
||||||
|
|
||||||
|
diff_is_single_cask(diff) && diff_only_version_or_checksum_changed(diff)
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff_is_single_cask(diff)
|
||||||
|
return false unless diff.files.count == 1
|
||||||
|
file = diff.files.first
|
||||||
|
return false unless file.a_path == file.b_path
|
||||||
|
file.a_path.match?(%r{\ACasks/[^/]+\.rb\Z})
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff_only_version_or_checksum_changed(diff)
|
||||||
|
lines = diff.files.flat_map(&:hunks).flat_map(&:lines)
|
||||||
|
|
||||||
|
additions = lines.select(&:addition?)
|
||||||
|
deletions = lines.select(&:deletion?)
|
||||||
|
changed_lines = deletions + additions
|
||||||
|
|
||||||
|
return false if additions.count != deletions.count
|
||||||
|
return false if additions.count > 2
|
||||||
|
|
||||||
|
changed_lines.all? { |line| diff_line_is_version(line.to_s) || diff_line_is_sha256(line.to_s) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff_line_is_sha256(line)
|
||||||
|
line.match?(/\A[+-]\s*sha256 '[0-9a-f]{64}'\Z/)
|
||||||
|
end
|
||||||
|
|
||||||
|
def diff_line_is_version(line)
|
||||||
|
line.match?(/\A[+-]\s*version '[^']+'\Z/)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.help
|
||||||
|
"automatically merge “simple” Cask pull requests"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -124,9 +124,9 @@ module GitHub
|
|||||||
@api_credentials_error_message ||= begin
|
@api_credentials_error_message ||= begin
|
||||||
unauthorized = (response_headers["http/1.1"] == "401 Unauthorized")
|
unauthorized = (response_headers["http/1.1"] == "401 Unauthorized")
|
||||||
scopes = response_headers["x-accepted-oauth-scopes"].to_s.split(", ")
|
scopes = response_headers["x-accepted-oauth-scopes"].to_s.split(", ")
|
||||||
needed_human_scopes = needed_scopes.join(", ")
|
if unauthorized && scopes.empty?
|
||||||
needed_human_scopes = "none" if needed_human_scopes.empty?
|
needed_human_scopes = needed_scopes.join(", ")
|
||||||
if !unauthorized && scopes.empty?
|
needed_human_scopes = "none" if needed_human_scopes.empty?
|
||||||
credentials_scopes = response_headers["x-oauth-scopes"]
|
credentials_scopes = response_headers["x-oauth-scopes"]
|
||||||
|
|
||||||
case GitHub.api_credentials_type
|
case GitHub.api_credentials_type
|
||||||
@ -154,7 +154,7 @@ module GitHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_api(url, data: nil, scopes: [].freeze)
|
def open_api(url, data: nil, request_method: nil, scopes: [].freeze)
|
||||||
# This is a no-op if the user is opting out of using the GitHub API.
|
# This is a no-op if the user is opting out of using the GitHub API.
|
||||||
return block_given? ? yield({}) : {} if ENV["HOMEBREW_NO_GITHUB_API"]
|
return block_given? ? yield({}) : {} if ENV["HOMEBREW_NO_GITHUB_API"]
|
||||||
|
|
||||||
@ -184,6 +184,10 @@ module GitHub
|
|||||||
data_tmpfile.write data
|
data_tmpfile.write data
|
||||||
data_tmpfile.close
|
data_tmpfile.close
|
||||||
args += ["--data", "@#{data_tmpfile.path}"]
|
args += ["--data", "@#{data_tmpfile.path}"]
|
||||||
|
|
||||||
|
if request_method
|
||||||
|
args += ["--request", request_method.to_s]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
args += ["--dump-header", headers_tmpfile.path]
|
args += ["--dump-header", headers_tmpfile.path]
|
||||||
@ -270,6 +274,31 @@ module GitHub
|
|||||||
search_issues(name, state: "open", repo: "#{tap.user}/homebrew-#{tap.repo}", in: "title")
|
search_issues(name, state: "open", repo: "#{tap.user}/homebrew-#{tap.repo}", in: "title")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
@user ||= open_api("#{API_URL}/user")
|
||||||
|
end
|
||||||
|
|
||||||
|
def permission(repo, user)
|
||||||
|
open_api("#{API_URL}/repos/#{repo}/collaborators/#{user}/permission")
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_access?(repo, user = nil)
|
||||||
|
user ||= self.user["login"]
|
||||||
|
["admin", "write"].include?(permission(repo, user)["permission"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def pull_requests(repo, base:, state: :open, **_options)
|
||||||
|
url = "#{API_URL}/repos/#{repo}/pulls?#{URI.encode_www_form(base: base, state: state)}"
|
||||||
|
open_api(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_pull_request(repo, number:, sha:, merge_method:, commit_message: nil)
|
||||||
|
url = "#{API_URL}/repos/#{repo}/pulls/#{number}/merge"
|
||||||
|
data = { sha: sha, merge_method: merge_method }
|
||||||
|
data[:commit_message] = commit_message if commit_message
|
||||||
|
open_api(url, data: data, request_method: :PUT, scopes: CREATE_ISSUE_FORK_OR_PR_SCOPES)
|
||||||
|
end
|
||||||
|
|
||||||
def print_pull_requests_matching(query)
|
def print_pull_requests_matching(query)
|
||||||
open_or_closed_prs = search_issues(query, type: "pr", user: "Homebrew")
|
open_or_closed_prs = search_issues(query, type: "pr", user: "Homebrew")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user