bundle_hack 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9b7e2f08fc8c5f36bb61d5a8e8627d57140b2018
4
- data.tar.gz: 96738bd9fffcc7f8a79280771c838db1d6a45595
2
+ SHA256:
3
+ metadata.gz: e3c07038f47d540e8eab90ea819c3a85e5672af8de6aca3e61e388cde3654406
4
+ data.tar.gz: 91dde30d342fee125343b625d3d7f5a4349cb6d6679208403de4b44fc81f7bf1
5
5
  SHA512:
6
- metadata.gz: 79c4ad0fd7c2c7d23480e80e9e9d4cfa5725700413c652b91da692a5fa70e5d5d32325f15c86685afba9e88df0e3446ff7602cd2f39da55a4839c02505ba72ed
7
- data.tar.gz: cf1e9f8f73b90d5f4874949d0c4db42039aba54feebfd3683684793e7615be8d1e008bad53cd6a1603b110ea7185a5f033e30bad8c5e9fdaccba5d4850acb2cf
6
+ metadata.gz: 8ae6b9769b44c75e47f67c6d72fe1a7286eeb2a21c89f14884742bf6f6eb1d3b5bcdae0798aeabb1394ba84efb06b07edb6a92a4312968bf82f823575e938e3d
7
+ data.tar.gz: 4b10042cc4bffe5271747702bb36fa5f66b0e0ae31e2536f26bf3852c3aa1c96557f325e46809b0bd43a80db1b5d80b13683ebff99adbce1fa6bd140cce1e70d
data/.gitignore CHANGED
@@ -14,3 +14,8 @@ Gemfile.lock
14
14
 
15
15
  *.swo
16
16
  *.swp
17
+
18
+ .byebug_history
19
+ spec/dummy/Gemfile.hack
20
+ spec/dummy/Gemfile.hack.lock
21
+ spec/dummy/hack/dummy_gem/dummy_gem.gemspec
@@ -1,4 +1,3 @@
1
- # Don't require top-level comment for all modules and classes
2
1
  Style/Documentation:
3
2
  Enabled: false
4
3
 
@@ -6,8 +5,9 @@ Metrics/BlockLength:
6
5
  Exclude:
7
6
  - 'spec/**/*'
8
7
 
9
- # We don't care about the auto-generated dummy Rails app
10
8
  AllCops:
11
9
  Exclude:
12
10
  - 'bin/**/*'
13
- - 'db/schema.rb'
11
+ - 'spec/dummy/**/*'
12
+ - 'spec/fixtures/**/*'
13
+ - 'config/header.rb'
@@ -0,0 +1 @@
1
+ 2.5.3
data/Gemfile CHANGED
@@ -4,5 +4,14 @@ source 'https://rubygems.org'
4
4
 
5
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
- # Specify your gem's dependencies in bundle_hack.gemspec
7
+ # These gems are only used by tests - we need to have these gems available in
8
+ # the current Bundler context (i.e. the context used when running tests) to
9
+ # allow us to use Bundler to locate gem sources etc., otherwise we would need to
10
+ # stub Bundler to hell. With Bundler being so integral to BundleHack I don't
11
+ # think that's an option.
12
+ gem 'another_dummy_gem',
13
+ git: 'https://github.com/bobf/another_dummy_gem', ref: 'ed0e5d21f1ff'
14
+ gem 'dummy_gem', '1.0.0'
15
+ gem 'noop', '0.0.2'
16
+
8
17
  gemspec
@@ -0,0 +1,5 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ bin/rspec
5
+ bin/rubocop
data/README.md CHANGED
@@ -32,10 +32,14 @@ The gem is copied to `hack/rake/` (relative to the same directory as your `Gemfi
32
32
 
33
33
  When you're done, you can restore a specific gem:
34
34
 
35
+ # TODO:
36
+
35
37
  ``` bash
36
38
  $ bundle hack:restore rake
37
39
  ```
38
40
 
41
+ # TODO:
42
+
39
43
  Or restore all hacked gems:
40
44
  ```
41
45
  $ bundle hack:restore:all
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
@@ -20,11 +20,16 @@ Gem::Specification.new do |spec|
20
20
  end
21
21
  end
22
22
  spec.bindir = 'bin'
23
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.executables = []
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_development_dependency 'bundler', '~> 1.16'
26
+ spec.add_dependency 'i18n', '>= 0.5'
27
+ spec.add_dependency 'parser', '~> 2.5'
28
+
29
+ spec.add_development_dependency 'byebug', '~> 10.0'
27
30
  spec.add_development_dependency 'rake', '~> 10.0'
28
31
  spec.add_development_dependency 'rspec', '~> 3.0'
29
- spec.add_development_dependency 'rubocop', '~> 0.59.2'
32
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
33
+ spec.add_development_dependency 'rubocop', '~> 0.60.0'
34
+ spec.add_development_dependency 'webmock', '~> 3.4'
30
35
  end
@@ -0,0 +1,4 @@
1
+
2
+ #
3
+ # BundleHack gems:
4
+ #
@@ -0,0 +1,6 @@
1
+ en:
2
+ errors:
3
+ missing_gem: "Requested gem '%{gem}' not found in Gemfile"
4
+ missing_spec: "Unable to locate .gemspec for '%{gem}'"
5
+ parsing: "Error parsing Gemfile: unable to locate '%{gem}'. Please file a bug report."
6
+ type: "Unknown type: %{type}"
@@ -1,7 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+ require 'yaml'
5
+
6
+ require 'i18n'
7
+ require 'parser/current'
8
+
9
+ require 'bundle_hack/command'
10
+ require 'bundle_hack/config_writer'
11
+ require 'bundle_hack/errors'
12
+ require 'bundle_hack/gem'
13
+ require 'bundle_hack/gem_cloner'
14
+ require 'bundle_hack/gem_locator'
15
+ require 'bundle_hack/gemfile_parser'
16
+ require 'bundle_hack/gemfile_writer'
17
+ require 'bundle_hack/gemspec_cloner'
18
+ require 'bundle_hack/sandbox'
19
+ require 'bundle_hack/gemspec_sources/base'
20
+ require 'bundle_hack/gemspec_sources/cache'
21
+ require 'bundle_hack/gemspec_sources/path'
22
+ require 'bundle_hack/gemspec_sources/rubygems'
3
23
  require 'bundle_hack/version'
4
24
 
25
+ I18n.config.available_locales = :en
26
+ I18n.load_path += Dir[
27
+ File.join(File.expand_path('..', __dir__), 'config', 'locales', '**', '*.yml')
28
+ ]
29
+
5
30
  module BundleHack
6
- # Your code goes here...
31
+ GEMFILE = 'Gemfile.hack'
32
+ HACK_DIR = 'hack'
33
+
34
+ def self.root
35
+ Pathname.new(File.dirname(__dir__))
36
+ end
7
37
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class Command < Bundler::Plugin::API
5
+ command 'hack'
6
+ command 'hack:restore'
7
+ command 'hack:restore:all'
8
+
9
+ def exec(command, args)
10
+ Sandbox.new(args.first).create if command == 'hack'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class ConfigWriter
5
+ def initialize(root_path)
6
+ @root_path = root_path
7
+ @config_path = File.join(@root_path, '.bundle', 'config')
8
+ end
9
+
10
+ def create_or_update
11
+ return create_config unless File.exist?(@config_path)
12
+
13
+ update_config
14
+ end
15
+
16
+ private
17
+
18
+ def create_config
19
+ FileUtils.mkdir_p(File.join(@root_path, '.bundle'))
20
+ write_config(config_settings)
21
+ end
22
+
23
+ def update_config
24
+ write_config(YAML.load_file(@config_path).merge(config_settings))
25
+ end
26
+
27
+ def write_config(config)
28
+ File.write(@config_path, config.to_yaml)
29
+ end
30
+
31
+ def config_settings
32
+ {
33
+ 'BUNDLE_GEMFILE' => GEMFILE
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class BundleHackError < StandardError; end
5
+ class MissingGemError < BundleHackError; end
6
+ class MissingSpecError < BundleHackError; end
7
+ class ParsingError < BundleHackError; end
8
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class Gem
5
+ attr_reader :name, :full_name, :version, :path
6
+
7
+ def initialize(definitions, options = {})
8
+ @definitions = definitions
9
+ @name = options.fetch(:name)
10
+ @full_name = options.fetch(:full_name)
11
+ @version = normalized_version(options.fetch(:version))
12
+ @path = Pathname.new(options.fetch(:path))
13
+ end
14
+
15
+ def locations
16
+ @definitions.map { |definition| definition[:locations] }.flatten
17
+ end
18
+
19
+ def params
20
+ find_params || {}
21
+ end
22
+
23
+ private
24
+
25
+ def find_params
26
+ # Because we don't know how the bundle was built (e.g. if it was built
27
+ # with `--without development test`) we have to take a guess at which
28
+ # definition to use for our parameters. In most cases the definition will
29
+ # be in the main block, otherwise we use the `development` definition and,
30
+ # failing that, we take the definition nearest to the end of the Gemfile.
31
+ # This is very much an edge case as this will only be a problem if we pick
32
+ # the wrong one AND the parameters are different to the "real" gem we're
33
+ # using.
34
+ return primary_definition[:params] unless primary_definition.nil?
35
+ return development_definition[:params] unless development_definition.nil?
36
+
37
+ any_definition[:params]
38
+ end
39
+
40
+ def primary_definition
41
+ @definitions.reverse_each.find do |definition|
42
+ definition.fetch(:groups).nil?
43
+ end
44
+ end
45
+
46
+ def development_definition
47
+ @definitions.reverse_each.find do |definition|
48
+ definition.fetch(:groups).include?(:development)
49
+ end
50
+ end
51
+
52
+ def any_definition
53
+ @definitions.last
54
+ end
55
+
56
+ def normalized_version(version)
57
+ # Ruby >= 2.3.0: `version` is a `Gem::Version`
58
+ # Ruby < 2.3.0: `version` is a `String`
59
+ version.respond_to?(:version) ? version.version : version
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class GemCloner
5
+ def initialize(gem, root_path)
6
+ @gem = gem
7
+ @root_path = root_path
8
+ end
9
+
10
+ def clone
11
+ hack_dir = @root_path.join(HACK_DIR)
12
+
13
+ FileUtils.mkdir_p(hack_dir)
14
+ # Sadly there is no `FileUtils.cp_R` so we copy and rename instead
15
+ FileUtils.cp_r(@gem.path, hack_dir)
16
+ FileUtils.mv(hack_dir.join(@gem.full_name), hack_dir.join(@gem.name))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class GemLocator
5
+ def initialize(gemfile_path, gemfile_lock_path)
6
+ @gemfile_path = gemfile_path
7
+ @gemfile_lock_path = gemfile_lock_path
8
+ end
9
+
10
+ def locate(gem_name)
11
+ gem = gems.find { |spec| spec[:name] == gem_name }
12
+ missing_gem_error(gem_name) if gem.nil?
13
+
14
+ gem
15
+ end
16
+
17
+ private
18
+
19
+ def gems
20
+ bundler_specs.map do |spec|
21
+ filtered_spec(spec)
22
+ end
23
+ end
24
+
25
+ def missing_gem_error(gem_name)
26
+ raise MissingGemError, I18n.t('errors.missing_gem', gem: gem_name)
27
+ end
28
+
29
+ def filtered_spec(spec)
30
+ {
31
+ name: spec.name,
32
+ full_name: spec.full_name,
33
+ path: spec.full_gem_path,
34
+ version: spec.version
35
+ }
36
+ end
37
+
38
+ def bundler_specs
39
+ @bundler_specs ||= Bundler::Definition.build(
40
+ @gemfile_path, @gemfile_lock_path, nil
41
+ ).specs
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class GemfileParser
5
+ def initialize(gemfile)
6
+ @gemfile = gemfile
7
+ end
8
+
9
+ def definitions_for(gem_name)
10
+ definitions(by_name(gem_name))
11
+ end
12
+
13
+ private
14
+
15
+ def by_name(gem_name)
16
+ sexps = gems.select do |gem|
17
+ gem[:sexp].children[2].to_sexp_array[1] == gem_name
18
+ end
19
+
20
+ parsing_error(gem_name) if sexps.empty?
21
+
22
+ sexps
23
+ end
24
+
25
+ def gems
26
+ top_level_gems + group_gems
27
+ end
28
+
29
+ def top_level_gems
30
+ gem_sexps_from_children(parsed_gemfile.children).map do |sexp|
31
+ { groups: nil, sexp: sexp }
32
+ end
33
+ end
34
+
35
+ def group_gems
36
+ parsed_gemfile.children.map do |child|
37
+ next unless group?(child)
38
+
39
+ gem_sexps_from_children(child.children[2..-1]).map do |sexp|
40
+ { groups: group_names(child.to_sexp_array), sexp: sexp }
41
+ end
42
+ end.compact.flatten(1)
43
+ end
44
+
45
+ def group_names(sexp_array)
46
+ # sexp_array[1] looks like:
47
+ # [:send, nil, :group, [:sym, :development], [:sym, :test]]
48
+ _send, _, _group, *group_pairs = sexp_array[1]
49
+ group_pairs.map { |_sym, group| group }
50
+ end
51
+
52
+ def group?(sexp)
53
+ sexp_array = sexp.to_sexp_array
54
+ sexp_array[0] == :block && sexp_array[1][2] == :group
55
+ end
56
+
57
+ def gem_sexps_from_children(children)
58
+ children.select do |child|
59
+ sexp_array = child.to_sexp_array
60
+ sexp_array[0] == :send && sexp_array[2] == :gem
61
+ end
62
+ end
63
+
64
+ def parsed_gemfile
65
+ @parsed_gemfile ||= begin
66
+ @gemfile.rewind
67
+ Parser::CurrentRuby.parse(@gemfile.read)
68
+ end
69
+ end
70
+
71
+ def definitions(gems)
72
+ gems.map do |gem|
73
+ {
74
+ groups: gem[:groups],
75
+ locations: location(gem[:sexp]),
76
+ params: hash_params(gem[:sexp])
77
+ }
78
+ end
79
+ end
80
+
81
+ def location(sexp)
82
+ (sexp.location.first_line..sexp.location.last_line).to_a
83
+ end
84
+
85
+ def hash_params(sexp)
86
+ last_arg_sexp = sexp.children.last
87
+ return nil unless last_arg_sexp.to_sexp_array.first == :hash
88
+
89
+ _, *pairs = last_arg_sexp.to_sexp_array
90
+
91
+ Hash[key_value_pairs(pairs)]
92
+ end
93
+
94
+ def key_value_pairs(pairs)
95
+ pairs.map do |_pair_sym, (_type, key), value|
96
+ [key, nested_value(*value)]
97
+ end
98
+ end
99
+
100
+ def nested_value(type, *values)
101
+ return primitive(type) if values.empty?
102
+ return array(values) if type == :array
103
+ return hash(values) if type == :hash
104
+ return values.first if %i[sym str].include?(type)
105
+
106
+ raise ParsingError, I18n.t('errors.type', type: type)
107
+ end
108
+
109
+ def array(values)
110
+ values.map { |type, value_array| nested_value(type, value_array) }
111
+ end
112
+
113
+ def hash(values)
114
+ Hash[array(values)]
115
+ end
116
+
117
+ def primitive(type)
118
+ # rubocop:disable Lint/BooleanSymbol
119
+ case type
120
+ when :false
121
+ false
122
+ when :true
123
+ true
124
+ when :nil
125
+ nil
126
+ end
127
+ # rubocop:enable Lint/BooleanSymbol
128
+ end
129
+
130
+ def parsing_error(gem_name)
131
+ raise ParsingError, I18n.t('errors.parsing', gem: gem_name)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class GemfileWriter
5
+ FILTERED_PARAMS = %i[git path].freeze
6
+
7
+ def initialize(root_path, gemfile_path, gems)
8
+ @root_path = root_path
9
+ @gemfile_path = gemfile_path
10
+ @hacked_gems = gems
11
+ @hacked_gemfile_path = @root_path.join(GEMFILE)
12
+ end
13
+
14
+ def create
15
+ comment_existing_gems
16
+ append_hacked_gems
17
+ end
18
+
19
+ private
20
+
21
+ def comment_existing_gems
22
+ File.write(@hacked_gemfile_path, commented_gemfile)
23
+ end
24
+
25
+ def commented_gemfile
26
+ File.readlines(@gemfile_path).to_enum.with_index(1).map do |line, index|
27
+ next line.chomp unless comment_lines.include?(index)
28
+
29
+ "# #{line.chomp} # managed by BundleHack"
30
+ end.join("\n")
31
+ end
32
+
33
+ def append_hacked_gems
34
+ File.write(
35
+ @hacked_gemfile_path,
36
+ File.read(@hacked_gemfile_path) + header + hacked_gem_definitions
37
+ )
38
+ end
39
+
40
+ def hacked_gem_definitions
41
+ @hacked_gems.map do |gem|
42
+ params = filtered_params(gem.params).merge(path: "hack/#{gem.name}")
43
+ "gem '#{gem.name}', #{params.inspect}"
44
+ end.join("\n")
45
+ end
46
+
47
+ def header
48
+ File.read(BundleHack.root.join('config', 'header.rb'))
49
+ end
50
+
51
+ def comment_lines
52
+ @hacked_gems.map(&:locations).flatten
53
+ end
54
+
55
+ def filtered_params(params)
56
+ params.reject { |key, _value| FILTERED_PARAMS.include?(key) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class GemspecCloner
5
+ def initialize(gem, root_path, options = {})
6
+ @use_cache = options.fetch(:use_cache, true)
7
+ @use_rubygems = options.fetch(:use_rubygems, true)
8
+ @gem = gem
9
+ @root_path = root_path
10
+ @spec_path = @root_path.join(HACK_DIR, @gem.name, "#{@gem.name}.gemspec")
11
+ end
12
+
13
+ def clone
14
+ FileUtils.mkdir_p(@root_path.join(HACK_DIR, @gem.name))
15
+ File.write(@spec_path, find_spec)
16
+ end
17
+
18
+ private
19
+
20
+ def find_spec
21
+ return path_spec unless path_spec.nil?
22
+ return cache_spec unless cache_spec.nil?
23
+ return rubygems_spec unless rubygems_spec.nil?
24
+
25
+ raise MissingSpecError, I18n.t('errors.missing_spec', gem: @gem.name)
26
+ end
27
+
28
+ def spec_from(source)
29
+ source.new(@gem).spec
30
+ end
31
+
32
+ def rubygems_spec
33
+ return nil unless @use_rubygems
34
+
35
+ @rubygems_spec ||= spec_from(GemspecSources::Rubygems)
36
+ end
37
+
38
+ def path_spec
39
+ @path_spec ||= spec_from(GemspecSources::Path)
40
+ end
41
+
42
+ def cache_spec
43
+ return nil unless @use_cache
44
+
45
+ @cache_spec ||= spec_from(GemspecSources::Cache)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ module GemspecSources
5
+ class Base
6
+ def initialize(gem)
7
+ @gem = gem
8
+ end
9
+
10
+ def spec
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ module GemspecSources
5
+ # I couldn't figure out how to do this with `Gem` directly so I've
6
+ # implemented it myself instead - if this can be collapsed into something
7
+ # along the lines of `Gem.from_cache(gem_name, gem_version)` then that would
8
+ # mean all of this code can go away.
9
+ class Cache < Base
10
+ def spec
11
+ path = cache_file_path
12
+ return nil if path.nil?
13
+
14
+ # rubocop:disable Security/MarshalLoad
15
+ Marshal.load(File.read(cache_file_path)).to_ruby
16
+ # rubocop:enable Security/MarshalLoad
17
+ end
18
+
19
+ private
20
+
21
+ def cache_file_path
22
+ cache_dirs.each do |cache_dir|
23
+ cache_file_path = cache_dir.join("#{@gem.full_name}.gemspec")
24
+ return cache_file_path if File.exist?(cache_file_path)
25
+ end
26
+
27
+ nil
28
+ end
29
+
30
+ def cache_dirs
31
+ Dir.entries(base_dir)
32
+ .select { |path| File.directory?(quick_cache(path)) }
33
+ .reject { |path| %w[. ..].include?(path) }
34
+ .map { |path| quick_cache(path) }
35
+ end
36
+
37
+ def quick_cache(path)
38
+ # I think the `4.8` versioning is highly stable. The lead dev of
39
+ # Bundler references it in this post from nearly 5 years ago:
40
+ # https://www.engineyard.com/blog/new-rubygems-index-format
41
+ # So I think # it's okay to make this a hard requirement.
42
+ base_dir.join(path, 'quick', 'Marshal.4.8')
43
+ end
44
+
45
+ def base_dir
46
+ @base_dir ||= Pathname.new(::Gem.spec_cache_dir)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ module GemspecSources
5
+ class Path < Base
6
+ def spec
7
+ # If the gem comes from a path (including git) then we will have the
8
+ # gemspec available in the gem directory (unlike when it comes from
9
+ # rubygems)
10
+ gemspec_path = @gem.path.join("#{@gem.name}.gemspec")
11
+ return nil unless File.exist?(gemspec_path)
12
+
13
+ File.read(gemspec_path)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ module GemspecSources
5
+ class Rubygems < Base
6
+ def spec
7
+ dependency = ::Gem::Dependency.new(@gem.name, @gem.version)
8
+
9
+ # Borrowed from:
10
+ # https://ruby-doc.org/stdlib-2.5.3/libdoc/rubygems/rdoc/Gem.html#method-c-latest_spec_for
11
+ spec_tuples, = ::Gem::SpecFetcher.fetcher.spec_for_dependency(
12
+ dependency
13
+ )
14
+ spec, = spec_tuples.first
15
+ return nil if spec.nil?
16
+
17
+ spec.to_ruby
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleHack
4
+ class Sandbox
5
+ def initialize(gem_name, gemfile_path = nil)
6
+ @gem_name = gem_name
7
+ @gemfile_path = gemfile_path || Bundler.default_gemfile
8
+ @gemfile_lock_path = lock_path(gemfile_path)
9
+ @root_path = Pathname.new(File.dirname(@gemfile_path))
10
+ end
11
+
12
+ def build
13
+ parser = GemfileParser.new(File.open(@gemfile_path))
14
+ gems = locate_gems(parser)
15
+ ConfigWriter.new(@root_path).create_or_update
16
+ GemfileWriter.new(@root_path, @gemfile_path, gems).create
17
+ gems.each do |gem|
18
+ GemCloner.new(gem, @root_path).clone
19
+ GemspecCloner.new(gem, @root_path).clone
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def locate_gems(parser)
26
+ # TODO: Read from .bundle_hack.yml
27
+ [@gem_name].map { |gem_name| locate_gem(gem_name, parser) }
28
+ end
29
+
30
+ def locate_gem(gem_name, parser)
31
+ spec = GemLocator.new(@gemfile_path, @gemfile_lock_path)
32
+ .locate(gem_name)
33
+ Gem.new(parser.definitions_for(gem_name), spec)
34
+ end
35
+
36
+ def lock_path(gemfile_path)
37
+ gemfile_path.nil? ? Bundler.default_lockfile : "#{gemfile_path}.lock"
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleHack
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundle_hack'
metadata CHANGED
@@ -1,29 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundle_hack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-26 00:00:00.000000000 Z
11
+ date: 2018-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: i18n
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parser
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '1.16'
33
+ version: '2.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
20
48
  type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
52
  - - "~>"
25
53
  - !ruby/object:Gem::Version
26
- version: '1.16'
54
+ version: '10.0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,41 +80,89 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-its
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.2'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: rubocop
57
99
  requirement: !ruby/object:Gem::Requirement
58
100
  requirements:
59
101
  - - "~>"
60
102
  - !ruby/object:Gem::Version
61
- version: 0.59.2
103
+ version: 0.60.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.60.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.4'
62
118
  type: :development
63
119
  prerelease: false
64
120
  version_requirements: !ruby/object:Gem::Requirement
65
121
  requirements:
66
122
  - - "~>"
67
123
  - !ruby/object:Gem::Version
68
- version: 0.59.2
124
+ version: '3.4'
69
125
  description: Debug fearlessly and restore when you're done
70
126
  email:
71
127
  - robertanthonyfarrell@gmail.com
72
- executables:
73
- - console
74
- - setup
128
+ executables: []
75
129
  extensions: []
76
130
  extra_rdoc_files: []
77
131
  files:
78
132
  - ".gitignore"
79
133
  - ".rspec"
80
134
  - ".rubocop.yml"
135
+ - ".ruby-version"
81
136
  - ".travis.yml"
82
137
  - Gemfile
138
+ - Makefile
83
139
  - README.md
84
140
  - Rakefile
85
141
  - bin/console
142
+ - bin/rake
143
+ - bin/rspec
144
+ - bin/rubocop
86
145
  - bin/setup
87
146
  - bundle_hack.gemspec
147
+ - config/header.rb
148
+ - config/locales/en.yml
88
149
  - lib/bundle_hack.rb
150
+ - lib/bundle_hack/command.rb
151
+ - lib/bundle_hack/config_writer.rb
152
+ - lib/bundle_hack/errors.rb
153
+ - lib/bundle_hack/gem.rb
154
+ - lib/bundle_hack/gem_cloner.rb
155
+ - lib/bundle_hack/gem_locator.rb
156
+ - lib/bundle_hack/gemfile_parser.rb
157
+ - lib/bundle_hack/gemfile_writer.rb
158
+ - lib/bundle_hack/gemspec_cloner.rb
159
+ - lib/bundle_hack/gemspec_sources/base.rb
160
+ - lib/bundle_hack/gemspec_sources/cache.rb
161
+ - lib/bundle_hack/gemspec_sources/path.rb
162
+ - lib/bundle_hack/gemspec_sources/rubygems.rb
163
+ - lib/bundle_hack/sandbox.rb
89
164
  - lib/bundle_hack/version.rb
165
+ - plugins.rb
90
166
  homepage: http://github.com/bobf/bundle_hack
91
167
  licenses: []
92
168
  metadata: {}
@@ -106,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
182
  version: '0'
107
183
  requirements: []
108
184
  rubyforge_project:
109
- rubygems_version: 2.5.2.2
185
+ rubygems_version: 2.7.6
110
186
  signing_key:
111
187
  specification_version: 4
112
188
  summary: Safely hack gems in your bundle