diff --git a/Library/Homebrew/dev-cmd/extract.rb b/Library/Homebrew/dev-cmd/extract.rb new file mode 100644 index 0000000000..9a5298aaaa --- /dev/null +++ b/Library/Homebrew/dev-cmd/extract.rb @@ -0,0 +1,180 @@ +#: * `extract` [`--force`] [`--version=`]: +#: Looks through repository history to find the of and +#: creates a copy in /Formula/@.rb. If the tap is +#: not installed yet, attempts to install/clone the tap before continuing. +#: +#: If `--force` is passed, the file at the destination will be overwritten +#: if it already exists. Otherwise, existing files will be preserved. +#: +#: If an argument is passed through `--version`, of +#: will be extracted and placed in the destination tap. Otherwise, the most +#: recent version that can be found will be used. + +require "cli_parser" +require "utils/git" +require "formulary" +require "tap" + +def with_monkey_patch + BottleSpecification.class_eval do + if method_defined?(:method_missing) + alias_method :old_method_missing, :method_missing + end + define_method(:method_missing) { |*| } + end + + Module.class_eval do + if method_defined?(:method_missing) + alias_method :old_method_missing, :method_missing + end + define_method(:method_missing) { |*| } + end + + Resource.class_eval do + if method_defined?(:method_missing) + alias_method :old_method_missing, :method_missing + end + define_method(:method_missing) { |*| } + end + + DependencyCollector.class_eval do + if method_defined?(:parse_symbol_spec) + alias_method :old_parse_symbol_spec, :parse_symbol_spec + end + define_method(:parse_symbol_spec) { |*| } + end + + if defined?(DependencyCollector::Compat) + DependencyCollector::Compat.class_eval do + if method_defined?(:parse_string_spec) + alias_method :old_parse_string_spec, :parse_string_spec + end + define_method(:parse_string_spec) { |*| } + end + end + + yield +ensure + BottleSpecification.class_eval do + if method_defined?(:old_method_missing) + alias_method :method_missing, :old_method_missing + undef :old_method_missing + end + end + + Module.class_eval do + if method_defined?(:old_method_missing) + alias_method :method_missing, :old_method_missing + undef :old_method_missing + end + end + + Resource.class_eval do + if method_defined?(:old_method_missing) + alias_method :method_missing, :old_method_missing + undef :old_method_missing + end + end + + DependencyCollector.class_eval do + if method_defined?(:old_parse_symbol_spec) + alias_method :parse_symbol_spec, :old_parse_symbol_spec + undef :old_parse_symbol_spec + end + end + + if defined?(DependencyCollector::Compat) + DependencyCollector::Compat.class_eval do + if method_defined?(:old_parse_string_spec) + alias_method :parse_string_spec, :old_parse_string_spec + undef :old_parse_string_spec + end + end + end +end + +module Homebrew + module_function + + def extract + Homebrew::CLI::Parser.parse do + flag "--version=" + switch :debug + switch :force + end + + # Expect exactly two named arguments: formula and tap + raise UsageError if ARGV.named.length != 2 + + destination_tap = Tap.fetch(ARGV.named[1]) + odie "Cannot extract formula to homebrew/core!" if destination_tap.core_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 now core tap is structured. + file = repo/"Formula/#{name}.rb" + + if args.version + ohai "Searching repository history" + version = args.version + rev = "HEAD" + test_formula = nil + 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? + ohai "Skipping revision #{rev} - file is empty at this revision" if ARGV.debug? + 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) + end + + # 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 = name.capitalize + versioned_name = Formulary.class_s("#{class_name}@#{version}") + result.gsub!("class #{class_name} < Formula", "class #{versioned_name} < Formula") + + path = destination_tap.path/"Formula/#{name}@#{version}.rb" + if path.exist? + unless ARGV.force? + odie <<~EOS + Destination formula already exists: #{path} + To overwrite it and continue anyways, run: + `brew extract #{name} --version=#{version} --tap=#{destination_tap.name} --force` + EOS + end + ohai "Overwriting existing formula at #{path}" if ARGV.debug? + path.delete + end + ohai "Writing formula for #{name} from revision #{rev} to #{path}" + path.write result + end + + # @private + def formula_at_revision(repo, name, file, rev) + return if rev.empty? + contents = Git.last_revision_of_file(repo, file, before_commit: rev) + contents.gsub!("@url=", "url ") + contents.gsub!("require 'brewkit'", "require 'formula'") + with_monkey_patch { Formulary.from_contents(name, file, contents) } + end +end diff --git a/Library/Homebrew/test/dev-cmd/extract_spec.rb b/Library/Homebrew/test/dev-cmd/extract_spec.rb new file mode 100644 index 0000000000..59e7cc1905 --- /dev/null +++ b/Library/Homebrew/test/dev-cmd/extract_spec.rb @@ -0,0 +1,32 @@ +describe "brew extract", :integration_test do + it "retrieves the specified version of formula, defaulting to most recent" do + path = Tap::TAP_DIRECTORY/"homebrew/homebrew-foo" + (path/"Formula").mkpath + target = Tap.from_path(path) + core_tap = CoreTap.new + core_tap.path.cd do + system "git", "init" + formula_file = setup_test_formula "testball" + system "git", "add", "--all" + system "git", "commit", "-m", "testball 0.1" + contents = File.read(formula_file) + contents.gsub!("testball-0.1", "testball-0.2") + File.write(formula_file, contents) + system "git", "add", "--all" + system "git", "commit", "-m", "testball 0.2" + end + expect { brew "extract", "testball", target.name } + .to be_a_success + + expect(path/"Formula/testball@0.2.rb").to exist + + expect(Formulary.factory(path/"Formula/testball@0.2.rb").version).to be == "0.2" + + expect { brew "extract", "testball", target.name, "--version=0.1" } + .to be_a_success + + expect(path/"Formula/testball@0.1.rb").to exist + + expect(Formulary.factory(path/"Formula/testball@0.1.rb").version).to be == "0.1" + end +end diff --git a/docs/Manpage.md b/docs/Manpage.md index 6aece81cdc..b949556211 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -790,6 +790,18 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note * `edit` `formula`: Open `formula` in the editor. + * `extract` [`--force`] `formula` `tap` [`--version=``version`]: + Looks through repository history to find the `version` of `formula` and + creates a copy in `tap`/Formula/`formula`@`version`.rb. If the tap is + not installed yet, attempts to install/clone the tap before continuing. + + If `--force` is passed, the file at the destination will be overwritten + if it already exists. Otherwise, existing files will be preserved. + + If an argument is passed through `--version`, `version` of `formula` + will be extracted and placed in the destination tap. Otherwise, the most + recent version that can be found will be used. + * `formula` `formula`: Display the path where `formula` is located. diff --git a/manpages/brew.1 b/manpages/brew.1 index abc714ed17..df2451c071 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -727,6 +727,16 @@ Open all of Homebrew for editing\. Open \fIformula\fR in the editor\. . .TP +\fBextract\fR [\fB\-\-force\fR] \fIformula\fR \fItap\fR [\fB\-\-version=\fR\fIversion\fR] +Looks through repository history to find the \fIversion\fR of \fIformula\fR and creates a copy in \fItap\fR/Formula/\fIformula\fR@\fIversion\fR\.rb\. If the tap is not installed yet, attempts to install/clone the tap before continuing\. +. +.IP +If \fB\-\-force\fR is passed, the file at the destination will be overwritten if it already exists\. Otherwise, existing files will be preserved\. +. +.IP +If an argument is passed through \fB\-\-version\fR, \fIversion\fR of \fIformula\fR will be extracted and placed in the destination tap\. Otherwise, the most recent version that can be found will be used\. +. +.TP \fBformula\fR \fIformula\fR Display the path where \fIformula\fR is located\. .