Merge pull request #20402 from Homebrew/head-url-must-always-specify-branch

Ensure that `head` Git URLs always specify a branch name
This commit is contained in:
Issy Long 2025-08-13 07:47:21 +00:00 committed by GitHub
commit d7b59fdfe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 133 additions and 38 deletions

View File

@ -3807,12 +3807,12 @@ class Formula
# If called as a method this provides just the {url} for the {SoftwareSpec}.
# If a block is provided you can also add {.depends_on} and {Patch}es just to the {.head} {SoftwareSpec}.
# The download strategies (e.g. `:using =>`) are the same as for {url}.
# `master` is the default branch for Git and doesn't need stating with a `branch:` parameter.
# Git repositories must always specify `branch:`.
#
# ### Example
#
# ```ruby
# head "https://we.prefer.https.over.git.example.com/.git"
# head "https://we.prefer.https.over.git.example.com/.git", branch: "main"
# ```
#
# ```ruby

View File

@ -179,17 +179,21 @@ module Homebrew
def audit_head_branch
return unless @online
return unless @strict
return if spec_name != :head
return unless Utils::Git.remote_exists?(url)
return if specs[:tag].present?
return if specs[:revision].present?
# Skip `resource` URLs as they use SHAs instead of branch specifiers.
return if name != owner.name
return unless url.end_with?(".git")
return unless Utils::Git.remote_exists?(url)
branch = Utils.popen_read("git", "ls-remote", "--symref", url, "HEAD")
.match(%r{ref: refs/heads/(.*?)\s+HEAD})&.to_a&.second
return if branch.blank? || branch == specs[:branch]
detected_branch = Utils.popen_read("git", "ls-remote", "--symref", url, "HEAD")
.match(%r{ref: refs/heads/(.*?)\s+HEAD})&.to_a&.second
problem "Specify the default branch as `branch: \"#{branch}\"`"
message = "Git `head` URL must specify a branch name"
message += " - try `branch: \"#{detected_branch}\"`" if detected_branch.present?
problem message if specs[:branch].blank? || detected_branch != specs[:branch]
end
def problem(text)

View File

@ -13,7 +13,7 @@ module RuboCop
# - `bottle :unneeded`/`:disable` and `bottle do` should not be simultaneously present
# - `stable do` should not be present without a `head` spec
# - `stable do` should not be present with only `url|checksum|mirror|version`
# - `head do` should not be present with only `url`
# - `head do` should not be present with only `url|branch`
class ComponentsRedundancy < FormulaCop
HEAD_MSG = "`head` and `head do` should not be simultaneously present"
BOTTLE_MSG = "`bottle :modifier` and `bottle do` should not be simultaneously present"
@ -54,8 +54,9 @@ module RuboCop
head_block = find_block(body_node, :head)
if head_block && !head_block.body.nil?
child_nodes = head_block.body.begin_type? ? head_block.body.child_nodes : [head_block.body]
if child_nodes.all? { |n| n.send_type? && n.method_name == :url }
problem "`head do` should not be present with only `url`"
shorthand_head_methods = [:url, :branch]
if child_nodes.all? { |n| n.send_type? && shorthand_head_methods.include?(n.method_name) }
problem "`head do` should not be present with only #{shorthand_head_methods.join("/")}"
end
end

View File

@ -11,7 +11,7 @@ RSpec.describe Homebrew::DevCmd::Bump do
content = <<~RUBY
desc "HEAD-only test formula"
homepage "https://brew.sh"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
RUBY
setup_test_formula("headonly", content)

View File

@ -285,7 +285,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "GPL-3.0-or-later"
end
RUBY
@ -301,7 +301,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license all_of: ["GPL-3.0-or-later", "MIT"]
end
RUBY
@ -317,7 +317,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "GPL-3.0-or-later" => { with: "LLVM-exception" }
end
RUBY
@ -332,7 +332,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "GPL-3.0-or-later" => { with: "zzz" }
end
RUBY
@ -351,7 +351,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "GPL-3.0-or-later" => { with: "#{deprecated_spdx_exception}" }
end
RUBY
@ -371,7 +371,7 @@ RSpec.describe Homebrew::FormulaAuditor do
fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "GPL-3.0-or-later"
end
RUBY
@ -385,7 +385,7 @@ RSpec.describe Homebrew::FormulaAuditor do
fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license any_of: ["GPL-3.0-or-later", "MIT"]
end
RUBY
@ -399,7 +399,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "0BSD"
end
RUBY
@ -416,7 +416,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license "0BSD"
end
RUBY
@ -433,7 +433,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license #{license_any_mismatch}
end
RUBY
@ -450,7 +450,7 @@ RSpec.describe Homebrew::FormulaAuditor do
formula_text = <<~RUBY
class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git"
head "https://github.com/cask/cask.git", branch: "main"
license #{license_any}
end
RUBY
@ -714,6 +714,78 @@ RSpec.describe Homebrew::FormulaAuditor do
expect(fa.problems).to be_empty
end
it "requires `branch:` to be specified for Git head URLs" do
fa = formula_auditor "foo", <<~RUBY, online: true
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://github.com/Homebrew/homebrew-test-bot.git"
end
RUBY
fa.audit_specs
# This is `.last` because the first problem is the unreachable stable URL.
expect(fa.problems.last[:message]).to match("Git `head` URL must specify a branch name")
end
it "suggests a detected default branch for Git head URLs" do
fa = formula_auditor "foo", <<~RUBY, online: true
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://github.com/Homebrew/homebrew-test-bot.git", branch: "master"
end
RUBY
message = "Git `head` URL must specify a branch name - try `branch: \"main\"`"
fa.audit_specs
# This is `.last` because the first problem is the unreachable stable URL.
expect(fa.problems.last[:message]).to match(message)
end
it "ignores a pre-existing correct HEAD branch name" do
fa = formula_auditor "foo", <<~RUBY, online: true
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://github.com/Homebrew/homebrew-test-bot.git", branch: "main"
end
RUBY
fa.audit_specs
expect(fa.problems).not_to match("Git `head` URL must specify a branch name")
end
it "ignores `branch:` for non-Git head URLs" do
fa = formula_auditor "foo", <<~RUBY, online: true
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://brew.sh/foo.tgz", branch: "develop"
end
RUBY
fa.audit_specs
expect(fa.problems).not_to match("Git `head` URL must specify a branch name")
end
it "ignores `branch:` for `resource` URLs" do
fa = formula_auditor "foo", <<~RUBY, online: true
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
resource "bar" do
url "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/bar.rb"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
end
end
RUBY
fa.audit_specs
expect(fa.problems).not_to match("Git `head` URL must specify a branch name")
end
it "allows versions with no throttle rate" do
fa = formula_auditor "bar", <<~RUBY, core_tap: true
class Bar < Formula
@ -770,7 +842,7 @@ RSpec.describe Homebrew::FormulaAuditor do
class Bar < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
end
RUBY
@ -783,7 +855,7 @@ RSpec.describe Homebrew::FormulaAuditor do
class BarAT1 < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
end
RUBY
@ -796,7 +868,7 @@ RSpec.describe Homebrew::FormulaAuditor do
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
end
RUBY

View File

@ -17,7 +17,7 @@ RSpec.describe Homebrew::Livecheck do
desc "Test formula"
homepage "https://brew.sh"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
livecheck do
url "https://formulae.brew.sh/api/formula/ruby.json"
@ -252,7 +252,7 @@ RSpec.describe Homebrew::Livecheck do
desc "Test formula with a duplicate URL"
homepage "https://github.com/Homebrew/brew.git"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
end
end

View File

@ -12,7 +12,7 @@ RSpec.describe Homebrew::Livecheck::SkipConditions do
desc "Test formula"
homepage "https://brew.sh"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
livecheck do
url "https://formulae.brew.sh/api/formula/ruby.json"
@ -34,7 +34,7 @@ RSpec.describe Homebrew::Livecheck::SkipConditions do
head_only: formula("test_head_only") do
desc "HEAD-only test formula"
homepage "https://brew.sh"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
end,
gist: formula("test_gist") do
desc "Gist test formula"

View File

@ -8,7 +8,7 @@ RSpec.describe Livecheck do
formula do
homepage "https://brew.sh"
url "https://brew.sh/test-0.0.1.tgz"
head "https://github.com/Homebrew/brew.git"
head "https://github.com/Homebrew/brew.git", branch: "main"
end
end
let(:livecheck_f) { described_class.new(f.class) }

View File

@ -25,7 +25,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::ComponentsRedundancy do
it "reports an offense if both `head` and `head do` are present" do
expect_offense(<<~RUBY)
class Foo < Formula
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
head do
^^^^^^^ FormulaAudit/ComponentsRedundancy: `head` and `head do` should not be simultaneously present
# stuff
@ -50,7 +50,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::ComponentsRedundancy do
it "reports no offenses if `stable do` is present with a `head` method" do
expect_no_offenses(<<~RUBY)
class Foo < Formula
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
stable do
# stuff
@ -82,17 +82,30 @@ RSpec.describe RuboCop::Cop::FormulaAudit::ComponentsRedundancy do
end
head do
^^^^^^^ FormulaAudit/ComponentsRedundancy: `head do` should not be present with only `url`
^^^^^^^ FormulaAudit/ComponentsRedundancy: `head do` should not be present with only url/branch
url "https://brew.sh/foo.git"
end
end
RUBY
end
it "reports an offense if `head do` is present with only `url` and `branch`" do
expect_offense(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
head do
^^^^^^^ FormulaAudit/ComponentsRedundancy: `head do` should not be present with only url/branch
url "https://brew.sh/foo.git", branch: "develop"
end
end
RUBY
end
it "reports no offenses if `stable do` is present with `url` and `depends_on`" do
expect_no_offenses(<<~RUBY)
class Foo < Formula
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "trunk"
stable do
url "https://brew.sh/foo-1.0.tgz"
@ -109,6 +122,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::ComponentsRedundancy do
head do
url "https://brew.sh/foo.git"
branch "develop"
depends_on "bar"
end
end

View File

@ -193,6 +193,7 @@ RSpec.describe Utils::AST::FormulaAST do
head do
url "https://brew.sh/foo.git"
branch "develop"
end
end
RUBY
@ -205,6 +206,7 @@ RSpec.describe Utils::AST::FormulaAST do
head do
url "https://brew.sh/foo.git"
branch "develop"
end
end
RUBY
@ -355,7 +357,7 @@ RSpec.describe Utils::AST::FormulaAST do
described_class.new <<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
end
RUBY
end
@ -364,7 +366,7 @@ RSpec.describe Utils::AST::FormulaAST do
<<~RUBY.chomp
class Foo < Formula
url "https://brew.sh/foo-1.0.tar.gz"
head "https://brew.sh/foo.git"
head "https://brew.sh/foo.git", branch: "develop"
bottle do
sha256 "f7b1fc772c79c20fddf621ccc791090bc1085fcef4da6cca03399424c66e06ca" => :sierra
@ -387,6 +389,7 @@ RSpec.describe Utils::AST::FormulaAST do
head do
url "https://brew.sh/foo.git"
branch "develop"
end
end
RUBY
@ -403,6 +406,7 @@ RSpec.describe Utils::AST::FormulaAST do
head do
url "https://brew.sh/foo.git"
branch "develop"
end
end
RUBY

View File

@ -841,7 +841,7 @@ end
Formulae can specify an alternate download for the upstream project's development/cutting-edge source (e.g. `master`/`main`/`trunk`) using [`head`](https://rubydoc.brew.sh/Formula#head-class_method), which can be activated by passing `--HEAD` when installing. Specifying it is done in the same manner as [`url`](https://rubydoc.brew.sh/Formula#url-class_method), with added conventions for fetching from version control repositories:
* Git repositories need `branch:` specified to fetch a branch other than "master". If the repository is very large, specify `only_path` to [limit the checkout to one path](Cask-Cookbook.md#git-urls).
* Git repositories **must always** specify `branch:`. If the repository is very large, specify `only_path` to [limit the checkout to one path](Cask-Cookbook.md#git-urls).
```sh
head "https://github.com/some/package.git", branch: "main"