| 
									
										
										
										
											2024-08-12 10:30:59 +01:00
										 |  |  | # typed: true # rubocop:disable Sorbet/StrictSigil | 
					
						
							| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  | require "abstract_command" | 
					
						
							| 
									
										
										
										
											2018-07-29 20:51:57 -04:00
										 |  |  | require "utils/git" | 
					
						
							|  |  |  | require "formulary" | 
					
						
							| 
									
										
										
										
											2020-08-18 00:23:23 +01:00
										 |  |  | require "software_spec" | 
					
						
							| 
									
										
										
										
											2018-07-29 20:51:57 -04:00
										 |  |  | require "tap" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module Homebrew | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |   module DevCmd | 
					
						
							|  |  |  |     class Extract < AbstractCommand | 
					
						
							|  |  |  |       BOTTLE_BLOCK_REGEX = /  bottle (?:do.+?end|:[a-z]+)\n\n/m | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       cmd_args do | 
					
						
							| 
									
										
										
										
											2024-07-06 10:57:51 -07:00
										 |  |  |         usage_banner "`extract` [`--version=`] [`--git-revision=`] [`--force`] <formula> <tap>" | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         description <<~EOS | 
					
						
							|  |  |  |           Look through repository history to find the most recent version of <formula> and | 
					
						
							|  |  |  |           create a copy in <tap>. Specifically, the command will create the new | 
					
						
							|  |  |  |           formula file at <tap>`/Formula/`<formula>`@`<version>`.rb`. If the tap is not | 
					
						
							|  |  |  |           installed yet, attempt to install/clone the tap before continuing. To extract | 
					
						
							|  |  |  |           a formula from a tap that is not `homebrew/core` use its fully-qualified form of | 
					
						
							|  |  |  |           <user>`/`<repo>`/`<formula>. | 
					
						
							|  |  |  |         EOS | 
					
						
							| 
									
										
										
										
											2024-06-28 15:46:00 -07:00
										 |  |  |         flag   "--git-revision=", | 
					
						
							|  |  |  |                description: "Search for the specified <version> of <formula> starting at <revision> instead of HEAD." | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         flag   "--version=", | 
					
						
							|  |  |  |                description: "Extract the specified <version> of <formula> instead of the most recent." | 
					
						
							|  |  |  |         switch "-f", "--force", | 
					
						
							|  |  |  |                description: "Overwrite the destination formula if it already exists." | 
					
						
							| 
									
										
										
										
											2019-03-17 18:39:47 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         named_args [:formula, :tap], number: 2, without_api: true | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2018-07-29 20:51:57 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |       sig { override.void } | 
					
						
							|  |  |  |       def run | 
					
						
							|  |  |  |         if (tap_with_name = args.named.first&.then { Tap.with_formula_name(_1) }) | 
					
						
							|  |  |  |           source_tap, name = tap_with_name | 
					
						
							|  |  |  |         else | 
					
						
							| 
									
										
										
										
											2024-12-03 17:43:22 -08:00
										 |  |  |           name = args.named.fetch(0).downcase | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |           source_tap = CoreTap.instance | 
					
						
							| 
									
										
										
										
											2020-04-14 17:50:54 +05:30
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         raise TapFormulaUnavailableError.new(source_tap, name) unless source_tap.installed? | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 17:43:22 -08:00
										 |  |  |         destination_tap = Tap.fetch(args.named.fetch(1)) | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         unless Homebrew::EnvConfig.developer? | 
					
						
							|  |  |  |           odie "Cannot extract formula to homebrew/core!" if destination_tap.core_tap? | 
					
						
							|  |  |  |           odie "Cannot extract formula to homebrew/cask!" if destination_tap.core_cask_tap? | 
					
						
							|  |  |  |           odie "Cannot extract formula to the same tap!" if destination_tap == source_tap | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         destination_tap.install unless destination_tap.installed? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         repo = source_tap.path | 
					
						
							| 
									
										
										
										
											2024-06-28 15:46:00 -07:00
										 |  |  |         start_rev = args.git_revision || "HEAD" | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         pattern = if source_tap.core_tap? | 
					
						
							|  |  |  |           [source_tap.new_formula_path(name), repo/"Formula/#{name}.rb"].uniq | 
					
						
							|  |  |  |         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"] | 
					
						
							| 
									
										
										
										
											2018-08-02 00:21:09 -04:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2019-03-17 18:39:47 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         if args.version | 
					
						
							|  |  |  |           ohai "Searching repository history" | 
					
						
							|  |  |  |           version = args.version | 
					
						
							|  |  |  |           version_segments = Gem::Version.new(version).segments if Gem::Version.correct?(version) | 
					
						
							|  |  |  |           rev = T.let(nil, T.nilable(String)) | 
					
						
							|  |  |  |           test_formula = T.let(nil, T.nilable(Formula)) | 
					
						
							|  |  |  |           result = "" | 
					
						
							|  |  |  |           loop do | 
					
						
							| 
									
										
										
										
											2024-06-28 15:46:00 -07:00
										 |  |  |             rev = rev.nil? ? start_rev : "#{rev}~1" | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern, before_commit: rev) | 
					
						
							|  |  |  |             if rev.nil? && source_tap.shallow? | 
					
						
							|  |  |  |               odie <<~EOS | 
					
						
							|  |  |  |                 Could not find #{name} but #{source_tap} is a shallow clone! | 
					
						
							|  |  |  |                 Try again after running: | 
					
						
							|  |  |  |                   git -C "#{source_tap.path}" fetch --unshallow | 
					
						
							|  |  |  |               EOS | 
					
						
							|  |  |  |             elsif rev.nil? | 
					
						
							|  |  |  |               odie "Could not find #{name}! The formula or version may not have existed." | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             file = repo/path | 
					
						
							|  |  |  |             result = Utils::Git.last_revision_of_file(repo, file, before_commit: rev) | 
					
						
							|  |  |  |             if result.empty? | 
					
						
							|  |  |  |               odebug "Skipping revision #{rev} - file is empty at this revision" | 
					
						
							|  |  |  |               next | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             test_formula = formula_at_revision(repo, name, file, rev) | 
					
						
							|  |  |  |             break if test_formula.nil? || test_formula.version == version | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if version_segments && Gem::Version.correct?(test_formula.version) | 
					
						
							|  |  |  |               test_formula_version_segments = Gem::Version.new(test_formula.version).segments | 
					
						
							|  |  |  |               if version_segments.length < test_formula_version_segments.length | 
					
						
							|  |  |  |                 odebug "Apply semantic versioning with #{test_formula_version_segments}" | 
					
						
							|  |  |  |                 break if version_segments == test_formula_version_segments.first(version_segments.length) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             odebug "Trying #{test_formula.version} from revision #{rev} against desired #{version}" | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           odie "Could not find #{name}! The formula or version may not have existed." if test_formula.nil? | 
					
						
							|  |  |  |         else | 
					
						
							| 
									
										
										
										
											2024-06-10 09:31:53 +01:00
										 |  |  |           # Search in the root directory of `repository` as well as recursively in all of its subdirectories. | 
					
						
							| 
									
										
										
										
											2024-06-28 15:46:00 -07:00
										 |  |  |           files = if start_rev == "HEAD" | 
					
						
							|  |  |  |             Dir[repo/"{,**/}"].filter_map do |dir| | 
					
						
							|  |  |  |               Pathname.glob("#{dir}/#{name}.rb").find(&:file?) | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             [] | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |           if files.empty? | 
					
						
							|  |  |  |             ohai "Searching repository history" | 
					
						
							| 
									
										
										
										
											2024-06-28 15:46:00 -07:00
										 |  |  |             rev, (path,) = Utils::Git.last_revision_commit_of_files(repo, pattern, before_commit: start_rev) | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             odie "Could not find #{name}! The formula or version may not have existed." if rev.nil? | 
					
						
							|  |  |  |             file = repo/path | 
					
						
							|  |  |  |             version = T.must(formula_at_revision(repo, name, file, rev)).version | 
					
						
							|  |  |  |             result = Utils::Git.last_revision_of_file(repo, file) | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             file = files.fetch(0).realpath | 
					
						
							|  |  |  |             rev = T.let("HEAD", T.nilable(String)) | 
					
						
							|  |  |  |             version = Formulary.factory(file).version | 
					
						
							|  |  |  |             result = File.read(file) | 
					
						
							| 
									
										
										
										
											2019-12-01 22:26:03 +08:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         # The class name has to be renamed to match the new filename, | 
					
						
							|  |  |  |         # e.g. Foo version 1.2.3 becomes FooAT123 and resides in Foo@1.2.3.rb. | 
					
						
							|  |  |  |         class_name = Formulary.class_s(name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-19 18:24:45 -04:00
										 |  |  |         # The version can only contain digits with decimals in between. | 
					
						
							|  |  |  |         version_string = version.to_s | 
					
						
							|  |  |  |                                 .sub(/\D*(.+?)\D*$/, "\\1") | 
					
						
							|  |  |  |                                 .gsub(/\D+/, ".") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Remove any existing version suffixes, as a new one will be added later. | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         name.sub!(/\b@(.*)\z\b/i, "") | 
					
						
							| 
									
										
										
										
											2024-06-19 18:24:45 -04:00
										 |  |  |         versioned_name = Formulary.class_s("#{name}@#{version_string}") | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         result.sub!("class #{class_name} < Formula", "class #{versioned_name} < Formula") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-19 18:24:45 -04:00
										 |  |  |         # Remove bottle blocks, as they won't work. | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         result.sub!(BOTTLE_BLOCK_REGEX, "") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-19 18:24:45 -04:00
										 |  |  |         path = destination_tap.path/"Formula/#{name}@#{version_string}.rb" | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         if path.exist? | 
					
						
							|  |  |  |           unless args.force? | 
					
						
							|  |  |  |             odie <<~EOS | 
					
						
							|  |  |  |               Destination formula already exists: #{path} | 
					
						
							|  |  |  |               To overwrite it and continue anyways, run: | 
					
						
							|  |  |  |                 brew extract --force --version=#{version} #{name} #{destination_tap.name} | 
					
						
							|  |  |  |             EOS | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           odebug "Overwriting existing formula at #{path}" | 
					
						
							|  |  |  |           path.delete | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-07-03 16:41:31 -07:00
										 |  |  |         ohai "Writing formula for #{name} at #{version} from revision #{rev} to:", path | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         path.dirname.mkpath | 
					
						
							|  |  |  |         path.write result | 
					
						
							| 
									
										
										
										
											2018-07-29 20:51:57 -04:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |       private | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |       sig { params(repo: Pathname, name: String, file: Pathname, rev: String).returns(T.nilable(Formula)) } | 
					
						
							|  |  |  |       def formula_at_revision(repo, name, file, rev) | 
					
						
							|  |  |  |         return if rev.empty? | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         contents = Utils::Git.last_revision_of_file(repo, file, before_commit: rev) | 
					
						
							|  |  |  |         contents.gsub!("@url=", "url ") | 
					
						
							|  |  |  |         contents.gsub!("require 'brewkit'", "require 'formula'") | 
					
						
							|  |  |  |         contents.sub!(BOTTLE_BLOCK_REGEX, "") | 
					
						
							|  |  |  |         with_monkey_patch { Formulary.from_contents(name, file, contents, ignore_errors: true) } | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |       def with_monkey_patch | 
					
						
							|  |  |  |         # Since `method_defined?` is not a supported type guard, the use of `alias_method` below is not typesafe: | 
					
						
							|  |  |  |         BottleSpecification.class_eval do | 
					
						
							|  |  |  |           T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing) | 
					
						
							| 
									
										
										
										
											2024-10-25 03:06:16 +08:00
										 |  |  |           define_method(:method_missing) do |*_| | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             # do nothing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         Module.class_eval do | 
					
						
							|  |  |  |           T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing) | 
					
						
							| 
									
										
										
										
											2024-10-25 03:06:16 +08:00
										 |  |  |           define_method(:method_missing) do |*_| | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             # do nothing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         Resource.class_eval do | 
					
						
							|  |  |  |           T.unsafe(self).alias_method :old_method_missing, :method_missing if method_defined?(:method_missing) | 
					
						
							| 
									
										
										
										
											2024-10-25 03:06:16 +08:00
										 |  |  |           define_method(:method_missing) do |*_| | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             # do nothing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         DependencyCollector.class_eval do | 
					
						
							|  |  |  |           if method_defined?(:parse_symbol_spec) | 
					
						
							|  |  |  |             T.unsafe(self).alias_method :old_parse_symbol_spec, | 
					
						
							|  |  |  |                                         :parse_symbol_spec | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2024-10-25 03:06:16 +08:00
										 |  |  |           define_method(:parse_symbol_spec) do |*_| | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |             # do nothing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         yield | 
					
						
							|  |  |  |       ensure | 
					
						
							|  |  |  |         BottleSpecification.class_eval do | 
					
						
							|  |  |  |           if method_defined?(:old_method_missing) | 
					
						
							|  |  |  |             T.unsafe(self).alias_method :method_missing, :old_method_missing | 
					
						
							|  |  |  |             undef :old_method_missing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         Module.class_eval do | 
					
						
							|  |  |  |           if method_defined?(:old_method_missing) | 
					
						
							|  |  |  |             T.unsafe(self).alias_method :method_missing, :old_method_missing | 
					
						
							|  |  |  |             undef :old_method_missing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Resource.class_eval do | 
					
						
							|  |  |  |           if method_defined?(:old_method_missing) | 
					
						
							|  |  |  |             T.unsafe(self).alias_method :method_missing, :old_method_missing | 
					
						
							|  |  |  |             undef :old_method_missing | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-21 08:24:37 -07:00
										 |  |  |         DependencyCollector.class_eval do | 
					
						
							|  |  |  |           if method_defined?(:old_parse_symbol_spec) | 
					
						
							|  |  |  |             T.unsafe(self).alias_method :parse_symbol_spec, :old_parse_symbol_spec | 
					
						
							|  |  |  |             undef :old_parse_symbol_spec | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-01-26 11:36:08 -08:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2018-07-29 20:51:57 -04:00
										 |  |  | end |