diff --git a/Library/Homebrew/cli/args.rbi b/Library/Homebrew/cli/args.rbi index 004dc2fb2..a40455d22 100644 --- a/Library/Homebrew/cli/args.rbi +++ b/Library/Homebrew/cli/args.rbi @@ -293,6 +293,9 @@ module Homebrew sig { returns(T::Boolean) } def write_only?; end + + sig { returns(T::Boolean) } + def custom_remote?; end end end end diff --git a/Library/Homebrew/cmd/tap.rb b/Library/Homebrew/cmd/tap.rb index f044c18df..c81b369aa 100644 --- a/Library/Homebrew/cmd/tap.rb +++ b/Library/Homebrew/cmd/tap.rb @@ -35,6 +35,8 @@ module Homebrew switch "--force-auto-update", description: "Auto-update tap even if it is not hosted on GitHub. By default, only taps "\ "hosted on GitHub are auto-updated (for performance reasons)." + switch "--custom-remote", + description: "Install or change a tap with a custom remote. Useful for mirrors." switch "--repair", description: "Migrate tapped formulae from symlink-based to directory-based structure." switch "--list-pinned", @@ -64,8 +66,9 @@ module Homebrew begin tap.install clone_target: args.named.second, force_auto_update: force_auto_update?(args: args), + custom_remote: args.custom_remote?, quiet: args.quiet? - rescue TapRemoteMismatchError => e + rescue TapRemoteMismatchError, TapNoCustomRemoteError => e odie e rescue TapAlreadyTappedError nil diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 02928cf3f..58aa822dc 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -324,13 +324,28 @@ class TapRemoteMismatchError < RuntimeError @expected_remote = expected_remote @actual_remote = actual_remote - super <<~EOS + super message + end + + def message + <<~EOS Tap #{name} remote mismatch. #{expected_remote} != #{actual_remote} EOS end end +# Raised when the remote of Homebrew/core does not match HOMEBREW_CORE_GIT_REMOTE. +class TapCoreRemoteMismatchError < TapRemoteMismatchError + def message + <<~EOS + Tap #{name} remote does mot match HOMEBREW_CORE_GIT_REMOTE. + #{expected_remote} != #{actual_remote} + Please set HOMEBREW_CORE_GIT_REMOTE="#{actual_remote}" and run `brew update` instead. + EOS + end +end + # Raised when a tap is already installed. class TapAlreadyTappedError < RuntimeError attr_reader :name @@ -344,6 +359,19 @@ class TapAlreadyTappedError < RuntimeError end end +# Raised when run `brew tap --custom-remote` without a remote URL. +class TapNoCustomRemoteError < RuntimeError + attr_reader :name + + def initialize(name) + @name = name + + super <<~EOS + Tap #{name} with option `--custom-remote` but without a remote URL. + EOS + end +end + # Raised when another Homebrew operation is already in progress. class OperationInProgressError < RuntimeError def initialize(name) diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 23b61b2c9..8d8a792c4 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -246,7 +246,8 @@ class Tap # @param force_auto_update [Boolean, nil] If present, whether to override the # logic that skips non-GitHub repositories during auto-updates. # @param quiet [Boolean] If set, suppress all output. - def install(quiet: false, clone_target: nil, force_auto_update: nil) + # @param custom_remote [Boolean] If set, change the tap's remote if already installed. + def install(quiet: false, clone_target: nil, force_auto_update: nil, custom_remote: false) require "descriptions" require "readall" @@ -257,9 +258,11 @@ class Tap odie "#{name} was moved. Tap homebrew/#{new_repo} instead." end + raise TapNoCustomRemoteError, name if custom_remote && clone_target.nil? + requested_remote = clone_target || default_remote - if installed? + if installed? && !custom_remote raise TapRemoteMismatchError.new(name, @remote, requested_remote) if clone_target && requested_remote != remote raise TapAlreadyTappedError, name if force_auto_update.nil? && !shallow? end @@ -268,6 +271,10 @@ class Tap Utils::Git.ensure_installed! if installed? + if requested_remote != remote # we are sure that clone_target is not nil and custom_remote is true here + fix_remote_configuration(requested_remote: requested_remote, quiet: quiet) + end + unless force_auto_update.nil? config["forceautoupdate"] = force_auto_update return @@ -358,20 +365,33 @@ class Tap end end - def fix_remote_configuration - return unless remote.include? "github.com" + def fix_remote_configuration(requested_remote: nil, quiet: false) + if requested_remote.present? + path.cd do + safe_system "git", "remote", "set-url", "origin", requested_remote + safe_system "git", "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*" + end + $stderr.ohai "#{name}: changed remote from #{remote} to #{requested_remote}" unless quiet + end current_upstream_head = path.git_origin_branch - return if path.git_origin_has_branch? current_upstream_head + return if requested_remote.blank? && path.git_origin_has_branch?(current_upstream_head) - safe_system "git", "-C", path, "fetch", "origin" + args = %w[fetch] + args << "--quiet" if quiet + args << "origin" + safe_system "git", "-C", path, *args path.git_origin_set_head_auto new_upstream_head = path.git_origin_branch + return if new_upstream_head == old_upstream_head + path.git_rename_branch old: current_upstream_head, new: new_upstream_head path.git_branch_set_upstream local: new_upstream_head, origin: new_upstream_head - ohai "#{name}: changed default branch name from #{current_upstream_head} to #{new_upstream_head}!" + return if quiet + + $stderr.ohai "#{name}: changed default branch name from #{current_upstream_head} to #{new_upstream_head}!" end # Uninstall this {Tap}. @@ -741,12 +761,18 @@ class CoreTap < Tap end # CoreTap never allows shallow clones (on request from GitHub). - def install(quiet: false, clone_target: nil, force_auto_update: nil) + def install(quiet: false, clone_target: nil, force_auto_update: nil, custom_remote: false) remote = Homebrew::EnvConfig.core_git_remote + requested_remote = clone_target || default_remote + + # The remote will changed again on `brew update` since remotes for Homebrew/core are mismatched + raise TapCoreRemoteMismatchError.new(name, remote, requested_remote) if requested_remote != remote + if remote != default_remote - $stderr.puts "HOMEBREW_CORE_GIT_REMOTE set: using #{remote} for Homebrew/core Git remote URL." + $stderr.puts "HOMEBREW_CORE_GIT_REMOTE set: using #{remote} for Homebrew/core Git remote." end - super(quiet: quiet, clone_target: remote, force_auto_update: force_auto_update) + + super(quiet: quiet, clone_target: remote, force_auto_update: force_auto_update, custom_remote: custom_remote) end # @private diff --git a/Library/Homebrew/test/tap_spec.rb b/Library/Homebrew/test/tap_spec.rb index f57455027..ae7ef7a25 100644 --- a/Library/Homebrew/test/tap_spec.rb +++ b/Library/Homebrew/test/tap_spec.rb @@ -289,6 +289,33 @@ describe Tap do }.to raise_error(TapRemoteMismatchError) end + it "raises an error when the remote for Homebrew/core doesn't match HOMEBREW_CORE_GIT_REMOTE" do + core_tap = described_class.fetch("Homebrew", "core") + wrong_remote = "#{Homebrew::EnvConfig.core_git_remote}-oops" + expect { + core_tap.install clone_target: wrong_remote + }.to raise_error(TapCoreRemoteMismatchError) + end + + it "raises an error when run `brew tap --custom-remote` without a custom remote (already installed)" do + setup_git_repo + already_tapped_tap = described_class.new("Homebrew", "foo") + expect(already_tapped_tap).to be_installed + + expect { + already_tapped_tap.install clone_target: nil, custom_remote: true + }.to raise_error(TapNoCustomRemoteError) + end + + it "raises an error when run `brew tap --custom-remote` without a custom remote (not installed)" do + not_tapped_tap = described_class.new("Homebrew", "bar") + expect(not_tapped_tap).not_to be_installed + + expect { + not_tapped_tap.install clone_target: nil, custom_remote: true + }.to raise_error(TapNoCustomRemoteError) + end + describe "force_auto_update" do before do setup_git_repo diff --git a/completions/bash/brew b/completions/bash/brew index a5027e605..c5b8e4c8b 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -1902,6 +1902,7 @@ _brew_tap() { case "${cur}" in -*) __brewcomp " + --custom-remote --debug --force-auto-update --full diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index 96b797f46..07facf960 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -1288,6 +1288,7 @@ __fish_brew_complete_arg 'style; and not __fish_seen_argument -l formula -l form __fish_brew_complete_cmd 'tap' 'Tap a formula repository' +__fish_brew_complete_arg 'tap' -l custom-remote -d 'Install or change a tap with a custom remote. Useful for mirrors' __fish_brew_complete_arg 'tap' -l debug -d 'Display any debugging information' __fish_brew_complete_arg 'tap' -l force-auto-update -d 'Auto-update tap even if it is not hosted on GitHub. By default, only taps hosted on GitHub are auto-updated (for performance reasons)' __fish_brew_complete_arg 'tap' -l full -d 'Convert a shallow clone to a full clone without untapping. Taps are only cloned as shallow clones if `--shallow` was originally passed' diff --git a/completions/zsh/_brew b/completions/zsh/_brew index cd2342def..8c454ad15 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -1579,6 +1579,7 @@ _brew_style() { # brew tap _brew_tap() { _arguments \ + '--custom-remote[Install or change a tap with a custom remote. Useful for mirrors]' \ '--debug[Display any debugging information]' \ '--force-auto-update[Auto-update tap even if it is not hosted on GitHub. By default, only taps hosted on GitHub are auto-updated (for performance reasons)]' \ '--full[Convert a shallow clone to a full clone without untapping. Taps are only cloned as shallow clones if `--shallow` was originally passed]' \ diff --git a/docs/Manpage.md b/docs/Manpage.md index f1b0aa547..efa868051 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -600,6 +600,8 @@ using protocols other than HTTPS, e.g. SSH, git, HTTP, FTP(S), rsync. Fetch tap as a shallow clone rather than a full clone. Useful for continuous integration. * `--force-auto-update`: Auto-update tap even if it is not hosted on GitHub. By default, only taps hosted on GitHub are auto-updated (for performance reasons). +* `--custom-remote`: + Install or change a tap with a custom remote. Useful for mirrors. * `--repair`: Migrate tapped formulae from symlink-based to directory-based structure. * `--list-pinned`: diff --git a/manpages/brew.1 b/manpages/brew.1 index 4c6d88d76..7fc6f43a3 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -831,6 +831,10 @@ Fetch tap as a shallow clone rather than a full clone\. Useful for continuous in Auto\-update tap even if it is not hosted on GitHub\. By default, only taps hosted on GitHub are auto\-updated (for performance reasons)\. . .TP +\fB\-\-custom\-remote\fR +Install or change a tap with a custom remote\. Useful for mirrors\. +. +.TP \fB\-\-repair\fR Migrate tapped formulae from symlink\-based to directory\-based structure\. .