Merge pull request #7900 from jonchang/sponsor-cmd
Add `brew sponsors` command
This commit is contained in:
		
						commit
						ed8c2616ca
					
				
							
								
								
									
										59
									
								
								Library/Homebrew/dev-cmd/sponsors.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								Library/Homebrew/dev-cmd/sponsors.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "cli/parser"
 | 
			
		||||
require "utils/github"
 | 
			
		||||
 | 
			
		||||
module Homebrew
 | 
			
		||||
  module_function
 | 
			
		||||
 | 
			
		||||
  def sponsors_args
 | 
			
		||||
    Homebrew::CLI::Parser.new do
 | 
			
		||||
      usage_banner <<~EOS
 | 
			
		||||
        `sponsors`
 | 
			
		||||
 | 
			
		||||
        Print a Markdown summary of Homebrew's GitHub Sponsors, suitable for pasting into a README.
 | 
			
		||||
      EOS
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def sponsors
 | 
			
		||||
    sponsors_args.parse
 | 
			
		||||
 | 
			
		||||
    sponsors = {
 | 
			
		||||
      "named" => [],
 | 
			
		||||
      "users" => 0,
 | 
			
		||||
      "orgs"  => 0,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    GitHub.sponsors_by_tier("Homebrew").each do |tier|
 | 
			
		||||
      sponsors["named"] += tier["sponsors"] if tier["tier"] >= 100
 | 
			
		||||
      sponsors["users"] += tier["count"]
 | 
			
		||||
      sponsors["orgs"] += tier["sponsors"].count { |s| s["type"] == "organization" }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    items = []
 | 
			
		||||
    items += sponsors["named"].map { |s| "[#{s["name"]}](https://github.com/#{s["login"]})" }
 | 
			
		||||
 | 
			
		||||
    anon_users = sponsors["users"] - sponsors["named"].length - sponsors["orgs"]
 | 
			
		||||
 | 
			
		||||
    items << if items.length > 1
 | 
			
		||||
      "#{anon_users} other users"
 | 
			
		||||
    else
 | 
			
		||||
      "#{anon_users} users"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if sponsors["orgs"] == 1
 | 
			
		||||
      items << "#{sponsors["orgs"]} organization"
 | 
			
		||||
    elsif sponsors["orgs"] > 1
 | 
			
		||||
      items << "#{sponsors["orgs"]} organizations"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    sponsor_text = if items.length > 2
 | 
			
		||||
      items[0..-2].join(", ") + " and #{items.last}"
 | 
			
		||||
    else
 | 
			
		||||
      items.join(" and ")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    puts "Homebrew is generously supported by #{sponsor_text} via [GitHub Sponsors](https://github.com/sponsors/Homebrew)."
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										7
									
								
								Library/Homebrew/test/dev-cmd/sponsors_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Library/Homebrew/test/dev-cmd/sponsors_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "cmd/shared_examples/args_parse"
 | 
			
		||||
 | 
			
		||||
describe "Homebrew.sponsors_args" do
 | 
			
		||||
  it_behaves_like "parseable arguments"
 | 
			
		||||
end
 | 
			
		||||
@ -49,6 +49,14 @@ describe GitHub do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "::sponsors_by_tier", :needs_network do
 | 
			
		||||
    it "errors on an unauthenticated token" do
 | 
			
		||||
      expect {
 | 
			
		||||
        subject.sponsors_by_tier("Homebrew")
 | 
			
		||||
      }.to raise_error(/INSUFFICIENT_SCOPES|FORBIDDEN/)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "::get_artifact_url", :needs_network do
 | 
			
		||||
    it "fails to find a nonexistant workflow" do
 | 
			
		||||
      expect {
 | 
			
		||||
 | 
			
		||||
@ -231,6 +231,15 @@ module GitHub
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def open_graphql(query, scopes: [].freeze)
 | 
			
		||||
    data = { query: query }
 | 
			
		||||
    result = open_api("https://api.github.com/graphql", scopes: scopes, data: data, request_method: "POST")
 | 
			
		||||
 | 
			
		||||
    raise Error, result["errors"].map { |e| "#{e["type"]}: #{e["message"]}" }.join("\n") if result["errors"].present?
 | 
			
		||||
 | 
			
		||||
    result["data"]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def raise_api_error(output, errors, http_code, headers, scopes)
 | 
			
		||||
    json = begin
 | 
			
		||||
      JSON.parse(output)
 | 
			
		||||
@ -393,30 +402,26 @@ module GitHub
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approved_reviews(user, repo, pr, commit: nil)
 | 
			
		||||
    url = "https://api.github.com/graphql"
 | 
			
		||||
    data = {
 | 
			
		||||
      query: <<~EOS,
 | 
			
		||||
        { repository(name: "#{repo}", owner: "#{user}") {
 | 
			
		||||
            pullRequest(number: #{pr}) {
 | 
			
		||||
              reviews(states: APPROVED, first: 100) {
 | 
			
		||||
                nodes {
 | 
			
		||||
                  author {
 | 
			
		||||
                    ... on User { email login name databaseId }
 | 
			
		||||
                    ... on Organization { email login name databaseId }
 | 
			
		||||
                  }
 | 
			
		||||
                  authorAssociation
 | 
			
		||||
                  commit { oid }
 | 
			
		||||
    query = <<~EOS
 | 
			
		||||
      { repository(name: "#{repo}", owner: "#{user}") {
 | 
			
		||||
          pullRequest(number: #{pr}) {
 | 
			
		||||
            reviews(states: APPROVED, first: 100) {
 | 
			
		||||
              nodes {
 | 
			
		||||
                author {
 | 
			
		||||
                  ... on User { email login name databaseId }
 | 
			
		||||
                  ... on Organization { email login name databaseId }
 | 
			
		||||
                }
 | 
			
		||||
                authorAssociation
 | 
			
		||||
                commit { oid }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      EOS
 | 
			
		||||
    }
 | 
			
		||||
    result = open_api(url, scopes: ["user:email"], data: data, request_method: "POST")
 | 
			
		||||
    raise Error, result["errors"] if result["errors"].present?
 | 
			
		||||
      }
 | 
			
		||||
    EOS
 | 
			
		||||
 | 
			
		||||
    reviews = result["data"]["repository"]["pullRequest"]["reviews"]["nodes"]
 | 
			
		||||
    result = open_graphql(query, scopes: ["user:email"])
 | 
			
		||||
    reviews = result["repository"]["pullRequest"]["reviews"]["nodes"]
 | 
			
		||||
 | 
			
		||||
    reviews.map do |r|
 | 
			
		||||
      next if commit.present? && commit != r["commit"]["oid"]
 | 
			
		||||
@ -493,20 +498,21 @@ module GitHub
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def sponsors_by_tier(user)
 | 
			
		||||
    url = "https://api.github.com/graphql"
 | 
			
		||||
    data = {
 | 
			
		||||
      query: <<~EOS,
 | 
			
		||||
          {
 | 
			
		||||
            organization(login: "#{user}") {
 | 
			
		||||
              sponsorsListing {
 | 
			
		||||
                tiers(first: 100) {
 | 
			
		||||
                  nodes {
 | 
			
		||||
                    monthlyPriceInDollars
 | 
			
		||||
                    adminInfo {
 | 
			
		||||
                    sponsorships(first: 100) {
 | 
			
		||||
                      totalCount
 | 
			
		||||
                      nodes {
 | 
			
		||||
                        sponsor { login }
 | 
			
		||||
    query = <<~EOS
 | 
			
		||||
        { organization(login: "#{user}") {
 | 
			
		||||
          sponsorsListing {
 | 
			
		||||
            tiers(first: 10, orderBy: {field: MONTHLY_PRICE_IN_CENTS, direction: DESC}) {
 | 
			
		||||
              nodes {
 | 
			
		||||
                monthlyPriceInDollars
 | 
			
		||||
                adminInfo {
 | 
			
		||||
                  sponsorships(first: 100, includePrivate: true) {
 | 
			
		||||
                    totalCount
 | 
			
		||||
                    nodes {
 | 
			
		||||
                      privacyLevel
 | 
			
		||||
                      sponsorEntity {
 | 
			
		||||
                        __typename
 | 
			
		||||
                        ... on Organization { login name }
 | 
			
		||||
                        ... on User { login name }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
@ -515,9 +521,35 @@ module GitHub
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      EOS
 | 
			
		||||
    }
 | 
			
		||||
    open_api(url, scopes: ["admin:org", "user"], data: data, request_method: "POST")
 | 
			
		||||
      }
 | 
			
		||||
    EOS
 | 
			
		||||
    result = open_graphql(query, scopes: ["admin:org", "user"])
 | 
			
		||||
 | 
			
		||||
    tiers = result["organization"]["sponsorsListing"]["tiers"]["nodes"]
 | 
			
		||||
 | 
			
		||||
    tiers.map do |t|
 | 
			
		||||
      tier = t["monthlyPriceInDollars"]
 | 
			
		||||
      raise Error, "Your token needs the 'admin:org' scope to access this API" if t["adminInfo"].nil?
 | 
			
		||||
 | 
			
		||||
      sponsorships = t["adminInfo"]["sponsorships"]
 | 
			
		||||
      count = sponsorships["totalCount"]
 | 
			
		||||
      sponsors = sponsorships["nodes"].map do |sponsor|
 | 
			
		||||
        next unless sponsor["privacyLevel"] == "PUBLIC"
 | 
			
		||||
 | 
			
		||||
        se = sponsor["sponsorEntity"]
 | 
			
		||||
        {
 | 
			
		||||
          "name"  => se["name"].presence || sponsor["login"],
 | 
			
		||||
          "login" => se["login"],
 | 
			
		||||
          "type"  => se["__typename"].downcase,
 | 
			
		||||
        }
 | 
			
		||||
      end.compact
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "tier"     => tier,
 | 
			
		||||
        "count"    => count,
 | 
			
		||||
        "sponsors" => sponsors,
 | 
			
		||||
      }
 | 
			
		||||
    end.compact
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_repo_license(user, repo)
 | 
			
		||||
 | 
			
		||||
@ -88,3 +88,5 @@ Secure password storage and syncing is provided by [1Password for Teams](https:/
 | 
			
		||||
Homebrew is a member of the [Software Freedom Conservancy](https://sfconservancy.org).
 | 
			
		||||
 | 
			
		||||
[](https://sfconservancy.org)
 | 
			
		||||
 | 
			
		||||
Homebrew is generously supported by [Zeno R.R. Davatz](https://github.com/zdavatz), [Randy Reddig](https://github.com/ydnar) and 262 other users via [GitHub Sponsors](https://github.com/sponsors/Homebrew).
 | 
			
		||||
 | 
			
		||||
@ -71,6 +71,7 @@ ruby
 | 
			
		||||
search
 | 
			
		||||
sh
 | 
			
		||||
shellenv
 | 
			
		||||
sponsors
 | 
			
		||||
style
 | 
			
		||||
switch
 | 
			
		||||
tap
 | 
			
		||||
 | 
			
		||||
@ -967,6 +967,11 @@ build systems would not find otherwise.
 | 
			
		||||
* `--env`:
 | 
			
		||||
  Use the standard `PATH` instead of superenv's when `std` is passed.
 | 
			
		||||
 | 
			
		||||
### `sponsors`
 | 
			
		||||
 | 
			
		||||
Print a Markdown summary of Homebrew's GitHub Sponsors, suitable for pasting
 | 
			
		||||
into a README.
 | 
			
		||||
 | 
			
		||||
### `style` [*`options`*] [*`file`*|*`tap`*|*`formula`*]
 | 
			
		||||
 | 
			
		||||
Check formulae or files for conformance to Homebrew style guidelines.
 | 
			
		||||
 | 
			
		||||
@ -1254,6 +1254,9 @@ Start a Homebrew build environment shell\. Uses our years\-battle\-hardened Home
 | 
			
		||||
\fB\-\-env\fR
 | 
			
		||||
Use the standard \fBPATH\fR instead of superenv\'s when \fBstd\fR is passed\.
 | 
			
		||||
.
 | 
			
		||||
.SS "\fBsponsors\fR"
 | 
			
		||||
Print a Markdown summary of Homebrew\'s GitHub Sponsors, suitable for pasting into a README\.
 | 
			
		||||
.
 | 
			
		||||
.SS "\fBstyle\fR [\fIoptions\fR] [\fIfile\fR|\fItap\fR|\fIformula\fR]"
 | 
			
		||||
Check formulae or files for conformance to Homebrew style guidelines\.
 | 
			
		||||
.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user