Skip to content

Commit 6700ab3

Browse files
authored
Merge pull request #20730 from Homebrew/migrate-command-not-found
Migrate Homebrew/command-not-found to Homebrew/brew
2 parents f8d82c0 + 4ded5a9 commit 6700ab3

18 files changed

+989
-4
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Command-not-found scheduled database updates
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- .github/workflows/command-not-found-db-update.yml
9+
pull_request:
10+
schedule:
11+
- cron: "0 0 * * *"
12+
workflow_dispatch:
13+
inputs:
14+
max-downloads:
15+
description: Maximum number of formulae to download when updating
16+
required: false
17+
18+
jobs:
19+
update-database:
20+
if: startsWith( github.repository, 'Homebrew/' )
21+
runs-on: macos-latest
22+
permissions:
23+
packages: write
24+
steps:
25+
- name: Set up Homebrew
26+
id: set-up-homebrew
27+
uses: Homebrew/actions/setup-homebrew@main
28+
29+
- name: Install oras for interacting with GitHub Packages
30+
run: brew install oras
31+
32+
- name: Pull executables.txt from GitHub Packages
33+
run: oras pull ghcr.io/homebrew/command-not-found/executables:latest
34+
35+
- name: Update database
36+
env:
37+
MAX_DOWNLOADS: ${{ github.event.inputs.max-downloads }}
38+
run: |
39+
set -eo pipefail
40+
41+
if [[ -n "$MAX_DOWNLOADS" ]]
42+
then
43+
MAX_DOWNLOADS_ARGS="--max-downloads $MAX_DOWNLOADS"
44+
fi
45+
46+
# Need to intentionally leave MAX_DOWNLOADS_ARGS unquoted.
47+
# shellcheck disable=SC2086
48+
brew which-update --update-existing --install-missing $MAX_DOWNLOADS_ARGS executables.txt --summary-file=$GITHUB_STEP_SUMMARY
49+
50+
- name: Output database stats
51+
run: brew which-update --stats executables.txt
52+
53+
- name: Log in to GitHub Packages
54+
if: github.ref == 'refs/heads/main'
55+
run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io --username brewtestbot --password-stdin
56+
57+
- name: Push to GitHub Packages
58+
if: github.ref == 'refs/heads/main'
59+
run: |
60+
oras push --artifact-type application/vnd.homebrew.command-not-found.executables \
61+
ghcr.io/homebrew/command-not-found/executables:latest \
62+
executables.txt:text/plain
63+
64+
- name: Check upload
65+
if: github.ref == 'refs/heads/main'
66+
run: |
67+
shasum --algorithm=256 executables.txt > executables.txt.sha256
68+
rm -f executables.txt
69+
oras pull ghcr.io/homebrew/command-not-found/executables:latest
70+
shasum --algorithm=256 --check executables.txt.sha256
71+
72+
- name: Upload database artifact
73+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
74+
with:
75+
name: executables-database
76+
path: executables.txt
77+
78+
delete-old-versions:
79+
needs: update-database
80+
runs-on: ubuntu-latest
81+
if: github.ref == 'refs/heads/main'
82+
permissions:
83+
packages: write
84+
steps:
85+
- name: Delete old versions from GitHub Packages
86+
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
87+
with:
88+
package-name: command-not-found/executables
89+
package-type: container
90+
min-versions-to-keep: 0
91+
delete-only-untagged-versions: true

.github/workflows/tests.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ jobs:
113113

114114
- name: Set up all Homebrew taps
115115
run: |
116-
brew tap homebrew/command-not-found
117116
brew tap homebrew/portable-ruby
118117
119118
# brew style doesn't like world writable directories
@@ -122,7 +121,6 @@ jobs:
122121
- name: Run brew style on official taps
123122
run: |
124123
brew style homebrew/test-bot \
125-
homebrew/command-not-found \
126124
homebrew/portable-ruby \
127125
homebrew/cask
128126
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
# License: MIT
5+
# The license text can be found in Library/Homebrew/command-not-found/LICENSE
6+
7+
require "abstract_command"
8+
require "utils/shell"
9+
10+
module Homebrew
11+
module Cmd
12+
class CommandNotFoundInit < AbstractCommand
13+
cmd_args do
14+
description <<~EOS
15+
Print instructions for setting up the command-not-found hook for your shell.
16+
If the output is not to a tty, print the appropriate handler script for your shell.
17+
18+
For more information, see:
19+
https://docs.brew.sh/Command-Not-Found
20+
EOS
21+
named_args :none
22+
end
23+
24+
sig { override.void }
25+
def run
26+
if $stdout.tty?
27+
help
28+
else
29+
init
30+
end
31+
end
32+
33+
sig { returns(T.nilable(Symbol)) }
34+
def shell
35+
Utils::Shell.parent || Utils::Shell.preferred
36+
end
37+
38+
sig { void }
39+
def init
40+
case shell
41+
when :bash, :zsh
42+
puts File.read(File.expand_path("#{File.dirname(__FILE__)}/../command-not-found/handler.sh"))
43+
when :fish
44+
puts File.expand_path("#{File.dirname(__FILE__)}/../command-not-found/handler.fish")
45+
else
46+
raise "Unsupported shell type #{shell}"
47+
end
48+
end
49+
50+
sig { void }
51+
def help
52+
case shell
53+
when :bash, :zsh
54+
puts <<~EOS
55+
# To enable command-not-found
56+
# Add the following lines to ~/.#{shell}rc
57+
58+
HOMEBREW_COMMAND_NOT_FOUND_HANDLER="$(brew --repository)/Library/Homebrew/command-not-found/handler.sh"
59+
if [ -f "$HOMEBREW_COMMAND_NOT_FOUND_HANDLER" ]; then
60+
source "$HOMEBREW_COMMAND_NOT_FOUND_HANDLER";
61+
fi
62+
EOS
63+
when :fish
64+
puts <<~EOS
65+
# To enable command-not-found
66+
# Add the following line to ~/.config/fish/config.fish
67+
68+
set HOMEBREW_COMMAND_NOT_FOUND_HANDLER (brew --repository)/Library/Homebrew/command-not-found/handler.fish
69+
if test -f $HOMEBREW_COMMAND_NOT_FOUND_HANDLER
70+
source $HOMEBREW_COMMAND_NOT_FOUND_HANDLER
71+
end
72+
EOS
73+
else
74+
raise "Unsupported shell type #{shell}"
75+
end
76+
end
77+
end
78+
end
79+
end
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
# License: MIT
5+
# The license text can be found in Library/Homebrew/command-not-found/LICENSE
6+
7+
require "abstract_command"
8+
require "api"
9+
10+
module Homebrew
11+
module Cmd
12+
class WhichFormula < AbstractCommand
13+
ENDPOINT = "internal/executables.txt"
14+
DATABASE_FILE = T.let((Homebrew::API::HOMEBREW_CACHE_API/ENDPOINT).freeze, Pathname)
15+
16+
cmd_args do
17+
description <<~EOS
18+
Show which formula(e) provides the given command.
19+
EOS
20+
switch "--explain",
21+
description: "Output explanation of how to get <command> by installing one of the providing formulae."
22+
switch "--skip-update",
23+
description: "Skip updating the executables database if any version exists on disk, no matter how old."
24+
named_args :command, min: 1
25+
end
26+
27+
sig { override.void }
28+
def run
29+
# NOTE: It probably doesn't make sense to use that on multiple commands since
30+
# each one might print multiple formulae
31+
args.named.each do |command|
32+
which_formula command, explain: args.explain?, skip_update: args.skip_update?
33+
end
34+
end
35+
36+
sig { params(skip_update: T::Boolean).void }
37+
def download_and_cache_executables_file!(skip_update: false)
38+
if DATABASE_FILE.exist? && !DATABASE_FILE.empty? &&
39+
(skip_update || (Time.now - Homebrew::EnvConfig.api_auto_update_secs.to_i) < DATABASE_FILE.mtime)
40+
return
41+
end
42+
43+
url = "#{Homebrew::EnvConfig.api_domain}/#{ENDPOINT}"
44+
45+
if ENV.fetch("CI", false)
46+
max_time = nil # allow more time in CI
47+
retries = Homebrew::EnvConfig.curl_retries.to_i
48+
else
49+
max_time = 10 # seconds
50+
retries = nil # do not retry by default
51+
end
52+
53+
args = Utils::Curl.curl_args(max_time:, retries:)
54+
args += %W[
55+
--compressed
56+
--speed-limit #{ENV.fetch("HOMEBREW_CURL_SPEED_LIMIT")}
57+
--speed-time #{ENV.fetch("HOMEBREW_CURL_SPEED_TIME")}
58+
]
59+
args.prepend("--time-cond", DATABASE_FILE.to_s) if DATABASE_FILE.exist? && !DATABASE_FILE.empty?
60+
61+
Utils::Curl.curl_download(*args, url, to: DATABASE_FILE)
62+
FileUtils.touch(DATABASE_FILE, mtime: Time.now)
63+
end
64+
65+
sig { params(cmd: String).returns(T::Array[String]) }
66+
def matches(cmd)
67+
DATABASE_FILE.readlines.select { |line| line.include?(cmd) }.map(&:chomp)
68+
end
69+
70+
# Test if we have to reject the given formula, i.e. not suggest it.
71+
sig { params(name: String).returns(T::Boolean) }
72+
def reject_formula?(name)
73+
f = begin
74+
Formula[name]
75+
rescue
76+
nil
77+
end
78+
f.nil? || f.latest_version_installed? || f.requirements.any? { |r| r.required? && !r.satisfied? }
79+
end
80+
81+
# Output explanation of how to get 'cmd' by installing one of the providing
82+
# formulae.
83+
sig { params(cmd: String, formulae: T::Array[String]).void }
84+
def explain_formulae_install(cmd, formulae)
85+
formulae.reject! { |f| reject_formula? f }
86+
87+
return if formulae.blank?
88+
89+
if formulae.size == 1
90+
puts <<~EOS
91+
The program '#{cmd}' is currently not installed. You can install it by typing:
92+
brew install #{formulae.first}
93+
EOS
94+
else
95+
puts <<~EOS
96+
The program '#{cmd}' can be found in the following formulae:
97+
* #{formulae * "\n * "}
98+
Try: brew install <selected formula>
99+
EOS
100+
end
101+
end
102+
103+
# if 'explain' is false, print all formulae that can be installed to get the
104+
# given command. If it's true, print them in human-readable form with an help
105+
# text.
106+
sig { params(cmd: String, explain: T::Boolean, skip_update: T::Boolean).void }
107+
def which_formula(cmd, explain: false, skip_update: false)
108+
download_and_cache_executables_file!(skip_update: skip_update)
109+
110+
cmd = cmd.downcase
111+
112+
formulae = (matches cmd).filter_map do |m|
113+
formula, cmds_text = m.split(":", 2)
114+
next if formula.nil? || cmds_text.nil?
115+
116+
cmds = cmds_text.split
117+
formula if !cmds.nil? && cmds.include?(cmd)
118+
end
119+
120+
return if formulae.blank?
121+
122+
formulae.map! do |formula|
123+
formula.sub(/\(.*\)$/, "")
124+
end
125+
126+
if explain
127+
explain_formulae_install(cmd, formulae)
128+
else
129+
puts formulae * "\n"
130+
end
131+
end
132+
end
133+
end
134+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright © 2014-2018 Homebrew contributors
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# License: MIT
2+
# The license text can be found in Library/Homebrew/command-not-found/LICENSE
3+
4+
function fish_command_not_found
5+
set -l cmd $argv[1]
6+
set -l txt
7+
8+
if not contains -- "$cmd" "-h" "--help" "--usage" "-?"
9+
set txt (brew which-formula --explain $cmd 2> /dev/null)
10+
end
11+
12+
if test -z "$txt"
13+
__fish_default_command_not_found_handler $cmd
14+
else
15+
string collect $txt
16+
end
17+
end
18+
19+
function __fish_command_not_found_handler --on-event fish_command_not_found
20+
fish_command_not_found $argv
21+
end

0 commit comments

Comments
 (0)