diff --git a/Library/Homebrew/dev-cmd/extract.rb b/Library/Homebrew/dev-cmd/extract.rb index dca97bc7ec..fb8aa976a6 100644 --- a/Library/Homebrew/dev-cmd/extract.rb +++ b/Library/Homebrew/dev-cmd/extract.rb @@ -81,7 +81,8 @@ module Homebrew Look through repository history to find the most recent version of and create a copy in `/Formula/``@``.rb`. If the tap is not - installed yet, attempt to install/clone the tap before continuing. + installed yet, attempt to install/clone the tap before continuing. To extract + a from a tap that is not homebrew/core use //. EOS flag "--version=", @@ -97,47 +98,70 @@ module Homebrew # Expect exactly two named arguments: formula and tap raise UsageError if ARGV.named.length != 2 + if ARGV.named.first !~ HOMEBREW_TAP_FORMULA_REGEX + name = ARGV.named.first.downcase + source_tap = CoreTap.instance + else + name = Regexp.last_match(3).downcase + source_tap = Tap.fetch(Regexp.last_match(1), Regexp.last_match(2)) + raise TapFormulaUnavailableError.new(source_tap, name) unless source_tap.installed? + end + destination_tap = Tap.fetch(ARGV.named.second) odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap? + odie "Cannot extract formula to the same tap!" if destination_tap == source_tap destination_tap.install unless destination_tap.installed? - name = ARGV.named.first.downcase - repo = CoreTap.instance.path - # Formulae can technically live in "/.rb" or - # "/Formula/.rb", but explicitly use the latter for now - # since that is how the core tap is structured. - file = repo/"Formula/#{name}.rb" + repo = source_tap.path + pattern = if source_tap.core_tap? + [repo/"Formula/#{name}.rb"] + else + # A formula can technically live in the root directory of a tap or in any of its subdirectories + [repo/"#{name}.rb", repo/"**/#{name}.rb"] + end if args.version ohai "Searching repository history" version = args.version rev = "HEAD" test_formula = nil + result = "" loop do - loop do - rev = Git.last_revision_commit_of_file(repo, file, before_commit: "#{rev}~1") - break if rev.empty? - break unless Git.last_revision_of_file(repo, file, before_commit: rev).empty? + rev, (path,) = Git.last_revision_commit_of_files(repo, pattern, before_commit: "#{rev}~1") + odie "Could not find #{name}! The formula or version may not have existed." if rev.nil? + file = repo/path + result = Git.last_revision_of_file(repo, file, before_commit: rev) + if result.empty? ohai "Skipping revision #{rev} - file is empty at this revision" if ARGV.debug? + next end + test_formula = formula_at_revision(repo, name, file, rev) break if test_formula.nil? || test_formula.version == version ohai "Trying #{test_formula.version} from revision #{rev} against desired #{version}" if ARGV.debug? end odie "Could not find #{name}! The formula or version may not have existed." if test_formula.nil? - result = Git.last_revision_of_file(repo, file, before_commit: rev) - elsif File.exist?(file) - rev = "HEAD" - version = Formulary.factory(file).version - result = File.read(file) else - ohai "Searching repository history" - rev = Git.last_revision_commit_of_file(repo, file) - version = formula_at_revision(repo, name, file, rev).version - odie "Could not find #{name}! The formula or version may not have existed." if rev.empty? - result = Git.last_revision_of_file(repo, file) + # Search in the root directory of as well as recursively in all of its subdirectories + files = Dir[repo/"{,**/}"].map do |dir| + Pathname.glob(["#{dir}/#{name}.rb"]).find(&:file?) + end.compact + + if files.empty? + ohai "Searching repository history" + rev, (path,) = Git.last_revision_commit_of_files(repo, pattern) + odie "Could not find #{name}! The formula or version may not have existed." if rev.nil? + file = repo/path + version = formula_at_revision(repo, name, file, rev).version + result = Git.last_revision_of_file(repo, file) + else + file = files.first.realpath + rev = "HEAD" + version = Formulary.factory(file).version + result = File.read(file) + end end # The class name has to be renamed to match the new filename, diff --git a/Library/Homebrew/utils/git.rb b/Library/Homebrew/utils/git.rb index e0bb176fed..875b07dc13 100644 --- a/Library/Homebrew/utils/git.rb +++ b/Library/Homebrew/utils/git.rb @@ -4,7 +4,11 @@ module Git module_function def last_revision_commit_of_file(repo, file, before_commit: nil) - args = [before_commit.nil? ? "--skip=1" : before_commit.split("..").first] + args = if before_commit.nil? + ["--skip=1"] + else + [before_commit.split("..").first] + end out, = Open3.capture3( HOMEBREW_SHIMS_PATH/"scm/git", "-C", repo, @@ -14,6 +18,28 @@ module Git out.chomp end + def last_revision_commit_of_files(repo, files, before_commit: nil) + args = if before_commit.nil? + ["--skip=1"] + else + [before_commit.split("..").first] + end + + # git log output format: + # + # + # + # ... + # return [, [file_path1, file_path2, ...]] + out, = Open3.capture3( + HOMEBREW_SHIMS_PATH/"scm/git", "-C", repo, "log", + "--pretty=format:%h", "--abbrev=7", "--max-count=1", + "--diff-filter=d", "--name-only", *args, "--", *files + ) + rev, *paths = out.chomp.split(/\n/).reject(&:empty?) + [rev, paths] + end + def last_revision_of_file(repo, file, before_commit: nil) relative_file = Pathname(file).relative_path_from(repo) diff --git a/docs/Manpage.md b/docs/Manpage.md index 948500a123..a47c0784ef 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -782,7 +782,8 @@ Homebrew repository for editing if no *`formula`* is provided. Look through repository history to find the most recent version of *`formula`* and create a copy in *`tap`*`/Formula/`*`formula`*`@`*`version`*`.rb`. If the tap is not -installed yet, attempt to install/clone the tap before continuing. +installed yet, attempt to install/clone the tap before continuing. To extract a +*`formula`* from a tap that is not homebrew/core use *`user`*/*`repo`*/*`formula`*. * `--version`: Extract the provided *`version`* of *`formula`* instead of the most recent. diff --git a/manpages/brew.1 b/manpages/brew.1 index 8675833751..0816a4624f 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -981,7 +981,7 @@ Generate the new formula in the provided tap, specified as \fIuser\fR\fB/\fR\fIr Open a formula in the editor set by \fBEDITOR\fR or \fBHOMEBREW_EDITOR\fR, or open the Homebrew repository for editing if no \fIformula\fR is provided\. . .SS "\fBextract\fR [\fIoptions\fR] \fIformula\fR \fItap\fR" -Look through repository history to find the most recent version of \fIformula\fR and create a copy in \fItap\fR\fB/Formula/\fR\fIformula\fR\fB@\fR\fIversion\fR\fB\.rb\fR\. If the tap is not installed yet, attempt to install/clone the tap before continuing\. +Look through repository history to find the most recent version of \fIformula\fR and create a copy in \fItap\fR\fB/Formula/\fR\fIformula\fR\fB@\fR\fIversion\fR\fB\.rb\fR\. If the tap is not installed yet, attempt to install/clone the tap before continuing\. To extract a \fIformula\fR from a tap that is not homebrew/core use \fIuser\fR/\fIrepo\fR/\fIformula\fR\. . .TP \fB\-\-version\fR