cartage 1.2 → 2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cartage'
4
+
5
+ ##
6
+ # Provide helper methods for testing Cartage and plug-ins using Minitest.
7
+ module Cartage::Minitest
8
+ #:nocov:
9
+
10
+ ##
11
+ # A helper to stub ENV lookups against an +env+ hash. If +options+ has a key
12
+ # <tt>:passthrough</tt>, will reach for the original ENV value.
13
+ #
14
+ # Tested thoroughly only with Minitest::Moar::Stubbing. This helper will
15
+ # probably be moved to a different gem in the future.
16
+ def stub_env(env, options = {}, *block_args, &block)
17
+ mock = lambda { |key|
18
+ env.fetch(key) { |k|
19
+ ENV.send(:'__minitest_stub__[]', k) if options[:passthrough]
20
+ }
21
+ }
22
+
23
+ if defined?(Minitest::Moar::Stubbing)
24
+ stub ENV, :[], mock, *block_args, &block
25
+ else
26
+ ENV.stub :[], mock, *block_args, &block
27
+ end
28
+ end
29
+
30
+ # A helper to stub capturing shell calls (<tt>`echo true`</tt> or <tt>%x(echo
31
+ # true)</tt>) to return +value+.
32
+ #
33
+ # This helper will probably be moved to a different gem in the future.
34
+ def stub_backticks(value)
35
+ Kernel.send(:alias_method, :__minitest_stub_backticks__, :`)
36
+ Kernel.send(:define_method, :`) { |*| value }
37
+ yield
38
+ ensure
39
+ Kernel.send(:remove_method, :`)
40
+ Kernel.send(:alias_method, :`, :__minitest_stub_backticks__)
41
+ Kernel.send(:remove_method, :__minitest_stub_backticks__)
42
+ end
43
+
44
+ # A helper to stub Cartage#repo_url to return +value+.
45
+ def stub_cartage_repo_url(value = nil, &block)
46
+ stub_instance_method Cartage, :repo_url, -> { value || 'git://host/repo-url.git' },
47
+ &block
48
+ end
49
+
50
+ ##
51
+ # An assertion that Pathname#write receives the +expected+ value.
52
+ def assert_pathname_write(expected)
53
+ string_io = StringIO.new
54
+ stub_instance_method Pathname, :write, ->(v) { string_io.write(v) } do
55
+ yield
56
+ assert_equal expected, String.new(string_io.string)
57
+ end
58
+ end
59
+
60
+ # A helper to stub Pathname#expand_path to return <tt>Pathname(value)</tt>.
61
+ def stub_pathname_expand_path(value, &block)
62
+ stub_instance_method Pathname, :expand_path, Pathname(value), &block
63
+ end
64
+
65
+ # A helper to define one or more methods (identified in +names+) on +target+
66
+ # with the same +block+, which defaults to an empty callable.
67
+ def disable_unsafe_method(target, *names, &block)
68
+ block ||= ->(*) {}
69
+ names.each { |name| target.define_singleton_method name, &block }
70
+ end
71
+
72
+ # A helper to stub Dir.chdir, which was not stubbing cleanly. This also
73
+ # asserts that the path provided to Dir.chdir will be the +expected+ value.
74
+ def stub_dir_chdir(expected)
75
+ assert_equal = method(:assert_equal)
76
+
77
+ Dir.singleton_class.send(:alias_method, :__minitest_stub_chdir__, :chdir)
78
+ Dir.singleton_class.send(:define_method, :chdir) do |path, &block|
79
+ assert_equal.(expected, path)
80
+ block.call(path) if block
81
+ end
82
+
83
+ yield
84
+ ensure
85
+ Dir.singleton_class.send(:remove_method, :chdir)
86
+ Dir.singleton_class.send(:alias_method, :chdir, :__minitest_stub_chdir__)
87
+ Dir.singleton_class.send(:remove_method, :__minitest_stub_chdir__)
88
+ end
89
+
90
+ # Stubs Cartage#run and asserts that the array of commands provided are
91
+ # matched for each call and that they are all consumed.
92
+ def stub_cartage_run(*expected)
93
+ expected = [ expected ].flatten(1)
94
+ stub_instance_method Cartage, :run, ->(v) { assert_equal expected.shift, v } do
95
+ yield
96
+ end
97
+ assert_empty expected
98
+ end
99
+
100
+ #:nocov:
101
+
102
+ private
103
+
104
+ # Ripped and slightly modified from minitest-stub-any-instance.
105
+ def stub_instance_method(klass, name, val_or_callable, &block)
106
+ if defined?(::Minitest::Moar::Stubbing)
107
+ instance_stub klass, name, val_or_callable, &block
108
+ elsif defined?(::Minitest::StubAnyInstance)
109
+ klass.stub_any_instance(name, val_or_callable, &block)
110
+ else
111
+ begin
112
+ new_name = "__minitest_stub_instance_method__#{name}"
113
+ owns_method = instance_method(name).owner == klass
114
+ klass.class_eval do
115
+ alias_method new_name, name if owns_method
116
+
117
+ define_method(name) do |*args|
118
+ if val_or_callable.respond_to?(:call)
119
+ instance_exec(*args, &val_or_callable)
120
+ else
121
+ val_or_callable
122
+ end
123
+ end
124
+ end
125
+
126
+ yield
127
+ ensure
128
+ klass.class_eval do
129
+ remove_method name
130
+ if owns_method
131
+ alias_method name, new_name
132
+ remove_method new_name
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ Minitest::Test.send(:include, self)
140
+ end
@@ -1,27 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Cartage
2
- # Cartage::Plugin is the basis of the Cartage plugin system. All plug-ins
3
- # must inherit from this class.
4
+ # Cartage::Plugin is the basis of the Cartage plug-in system that extends
5
+ # core functionality. All plug-ins of this sort must inherit from this class.
6
+ # (Command extensions are handled by extending Cartage::CLI.)
4
7
  class Plugin
5
8
  class << self
6
- # Register a subclass.
7
- def inherited(klass) #:nodoc:
8
- registered << klass
9
+ # Register a plugin.
10
+ def inherited(plugin) #:nodoc:
11
+ registered[plugin.plugin_name] = plugin
9
12
  end
10
13
 
11
- # The plugins registered with Cartage.
14
+ # The plugins registered with Cartage as a dictionary of plug-in name
15
+ # (the key used for plug-in configuration) and plug-in class.
12
16
  def registered
13
- @registered ||= []
17
+ @registered ||= {}
18
+ end
19
+
20
+ # The name of the plugin.
21
+ def plugin_name
22
+ @name ||= name.split(/::/).last.gsub(/([A-Z])/, '_\1').downcase.
23
+ sub(/^_/, '')
24
+ end
25
+
26
+ # Iterate the plugins by +name+.
27
+ def each # :yields: name
28
+ registered.each_key { |k| yield k }
29
+ end
30
+
31
+ # The version of the plug-in.
32
+ def version
33
+ if const_defined?(:VERSION, false)
34
+ VERSION
35
+ else
36
+ Cartage::VERSION
37
+ end
38
+ end
39
+
40
+ # A utility method to load and decorate an object with Cartage plug-ins.
41
+ def load_for(klass) #:nodoc:
42
+ load
43
+ decorate(klass)
14
44
  end
15
45
 
16
- # A utility method that will find all Cartage plugins and load them. A
17
- # Cartage plugin is found with 'cartage/*.rb' and descends from
18
- # Cartage::Plugin.
46
+ # A utility method that will find all Cartage plug-ins and load them. A
47
+ # Cartage plug-in is found in the Gems as <tt>cartage/plugins/*.rb</tt>
48
+ # and descends from Cartage::Plugin.
19
49
  def load #:nodoc:
20
50
  @found ||= {}
21
51
  @loaded ||= {}
22
- @files ||= Gem.find_files('cartage/*.rb')
52
+ @files ||= Gem.find_files('cartage/plugins/*.rb')
23
53
 
24
- @files.reverse.each do |path|
54
+ @files.reverse_each do |path|
25
55
  name = File.basename(path, '.rb').to_sym
26
56
  @found[name] = path
27
57
  end
@@ -33,16 +63,15 @@ class Cartage
33
63
 
34
64
  # Decorate the provided class with lazy initialization methods.
35
65
  def decorate(klass) #:nodoc:
36
- registered.each do |plugin|
37
- name = plugin.name.split(/::/).last.gsub(/([A-Z])/, '_\1').downcase.
38
- sub(/^_/, '')
66
+ registered.each do |name, plugin|
39
67
  ivar = "@#{name}"
40
68
 
69
+ klass.send(:undef_method, name) if klass.method_defined?(name)
41
70
  klass.send(:define_method, name) do
42
71
  instance = instance_variable_defined?(ivar) &&
43
72
  instance_variable_get(ivar)
44
73
 
45
- instance ||= instance_variable_set(ivar, plugin.new(self))
74
+ instance || instance_variable_set(ivar, plugin.new(self))
46
75
  end
47
76
  end
48
77
  end
@@ -59,22 +88,105 @@ class Cartage
59
88
  end
60
89
  end
61
90
 
62
- # These are the command classes provided to the cartage binary. Despite the
63
- # name being plural, the return can either be a single CmdParse::Command
64
- # class, or an array of CmdParse::Command class. These command classes
65
- # should inherit from Cartage::Command, since they will be initialized with
66
- # a cartage parameter.
67
- def self.commands
68
- []
91
+ # Returns +true+ if the feature identified by +name+ is offered by this
92
+ # plug-in. Requires that subclasses implement the #offer_feature? private
93
+ # method.
94
+ def offer?(name)
95
+ enabled? && offer_feature?(name.to_sym)
69
96
  end
70
97
 
71
- # A plug-in is initialized with the Cartage instance that owns it.
98
+ # Returns +true+ if the plug-in has been explicitly disabled in
99
+ # configuration.
100
+ def disabled?
101
+ !!@disabled
102
+ end
103
+
104
+ # Returns +true+ if the plug-in is enabled (it has not been explicitly
105
+ # disabled).
106
+ def enabled?
107
+ !disabled?
108
+ end
109
+
110
+ # The name of this plug-in.
111
+ def plugin_name
112
+ self.class.plugin_name
113
+ end
114
+
115
+ # The version of this plug-in
116
+ def version
117
+ self.class.version
118
+ end
119
+
120
+ # A plug-in is given, as +cartage+, the instance of Cartage that owns it.
72
121
  def initialize(cartage)
122
+ fail NotImplementedError, 'not a subclass' if instance_of?(Cartage::Plugin)
73
123
  @cartage = cartage
124
+ @disabled = false
74
125
  end
75
126
 
76
127
  private
77
- def resolve_config!(*)
128
+
129
+ # The instance of Cartage that owns this plug-in instance.
130
+ attr_reader :cartage # :doc:
131
+
132
+ # This method should not be overridden by implementors.
133
+ def resolve_config!(config)
134
+ @disabled = config.disabled
135
+ resolve_plugin_config!(config)
136
+ end
137
+
138
+ # Plug-ins that have configuration that needs resolution before use should
139
+ # implement this method. It is provided +config+, which is just the subset
140
+ # of the overall Cartage::Config object that applies to this plug-in.
141
+ def resolve_plugin_config!(config) # :doc:
142
+ end
143
+
144
+ # This is an extension point for plug-ins to indicate that they offer the
145
+ # feature with the given +name+. The default implementation should be
146
+ # sufficient for most plug-ins, because it matches the way that plug-in
147
+ # methods are called.
148
+ def offer_feature?(name) # :doc:
149
+ respond_to?(name)
150
+ end
151
+ end
152
+
153
+ # A collection of Cartage::Plugin held by a Cartage instance.
154
+ class Plugins
155
+ def initialize # :nodoc:
156
+ @plugins = []
157
+ end
158
+
159
+ # Adds one or more +plugins+ to the collection.
160
+ def add(*plugins)
161
+ @plugins.push(*plugins)
162
+ end
163
+
164
+ # Return enabled plug-ins.
165
+ def enabled
166
+ @plugins.select(&:enabled?)
167
+ end
168
+
169
+ # Map +method+ over all plug-ins offering +feature+. Used to collect data
170
+ # from plug-ins that offer this +feature+.
171
+ def request_map(feature, method = feature)
172
+ feature = feature.to_sym
173
+ method = method.to_sym
174
+ offering(feature).map(&method)
175
+ end
176
+
177
+ # Run +method+ on plug-ins offering +feature+. Used to run code in plug-ins
178
+ # that offer this +feature+.
179
+ def request(feature, method = feature)
180
+ feature = feature.to_sym
181
+ method = method.to_sym
182
+ offering(feature).each(&method)
183
+ end
184
+
185
+ private
186
+
187
+ def offering(name)
188
+ name = name.to_sym
189
+ @plugins.select { |plugin| plugin.offer?(name) }
78
190
  end
79
191
  end
80
192
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Create the release package as a tarball.
5
+ #
6
+ # Offers:
7
+ # * +:build_package+
8
+ class Cartage::BuildTarball < Cartage::Plugin
9
+ # Create the package.
10
+ #
11
+ # Requests:
12
+ # * +:pre_build_tarball+
13
+ # * +:post_build_tarball+
14
+ def build_package
15
+ cartage.plugins.request(:pre_build_tarball)
16
+ run_command
17
+ cartage.plugins.request(:post_build_tarball)
18
+ end
19
+
20
+ # The final tarball name.
21
+ def package_name
22
+ @package_name ||=
23
+ Pathname("#{cartage.final_name}.tar#{cartage.tar_compression_extension}")
24
+ end
25
+
26
+ private
27
+
28
+ def run_command
29
+ command = [
30
+ 'tar',
31
+ "cf#{cartage.tar_compression_flag}",
32
+ package_name.to_s,
33
+ '-C',
34
+ cartage.tmp_path.to_s,
35
+ cartage.name
36
+ ]
37
+
38
+ cartage.run command
39
+ end
40
+ end
@@ -1,27 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tempfile'
4
+
2
5
  # Manage and use the package manifest ('Manifest.txt') and the ignore file
3
6
  # ('.cartignore').
4
7
  class Cartage::Manifest < Cartage::Plugin
5
8
  # This exception is raised if the package manifest is missing.
6
- MissingError = Class.new(StandardError) do
7
- def message
9
+ class MissingError < StandardError
10
+ def message # :nodoc:
8
11
  <<-exception
9
12
  Cartage cannot create a package without a Manifest.txt file. You may generate
10
13
  or update the Manifest.txt file with the following command:
11
14
 
15
+ cartage manifest generate
16
+
12
17
  exception
13
18
  end
14
19
  end
15
20
 
16
- DIFF = if system('gdiff', __FILE__, __FILE__) #:nodoc:
17
- 'gdiff'
18
- else
19
- 'diff'
20
- end
21
-
22
- def initialize(cartage) #:nodoc:
23
- @cartage = cartage
24
- end
21
+ DIFF = if system('gdiff', __FILE__, __FILE__) #:nodoc:
22
+ 'gdiff'
23
+ else
24
+ 'diff'
25
+ end
25
26
 
26
27
  # Resolve the Manifest to something that can be used by <tt>tar -T</tt>. The
27
28
  # manifest should be relative to the files in the repository, as reported
@@ -35,14 +36,17 @@ or update the Manifest.txt file with the following command:
35
36
  # front of every line in the Manifest.
36
37
  #
37
38
  # If +path+ is not provided, Dir.pwd is used.
38
- def resolve(path = nil, command: nil) # :yields: resolved manifest filename
39
- raise MissingError unless manifest_file.exist?
39
+ #
40
+ # A block is required and is provided the +resolved_path+.
41
+ def resolve(path = nil) # :yields: resolved_path
42
+ fail MissingError unless manifest_file.exist?
43
+ fail ArgumentError, 'A block is required.' unless block_given?
40
44
 
41
45
  data = strip_comments_and_empty_lines(manifest_file.readlines)
42
- raise "Manifest.txt is empty." if data.empty?
46
+ fail 'Manifest.txt is empty.' if data.empty?
43
47
 
44
48
  path = Pathname(path || Dir.pwd).expand_path.basename
45
- tmpfile = Tempfile.new('Manifest')
49
+ tmpfile = Tempfile.new('Manifest.')
46
50
 
47
51
  tmpfile.puts prune(data, with_slugignore: true).map { |line|
48
52
  path.join(line).to_s
@@ -65,20 +69,29 @@ or update the Manifest.txt file with the following command:
65
69
 
66
70
  # Checks Manifest.txt
67
71
  def check
68
- raise MissingError unless manifest_file.exist?
72
+ fail MissingError unless manifest_file.exist?
69
73
  tmp = create_file_list('Manifest.tmp')
70
- system(DIFF, '-du', manifest_file.basename.to_s, tmp.to_s)
74
+
75
+ args = [ DIFF, '-du', manifest_file.basename.to_s, tmp.to_s ]
76
+
77
+ if cartage.quiet
78
+ %x(#{(args << '-q').join(' ')})
79
+ else
80
+ system(*args)
81
+ end
82
+
71
83
  $?.success?
72
84
  ensure
73
85
  tmp.unlink if tmp
74
86
  end
75
87
 
76
- # Installs the default .cartignore file.
88
+ # Installs the default .cartignore file. Will either +overwrite+ or +merge+
89
+ # based on the provided +mode+.
77
90
  def install_default_ignore(mode: nil)
78
- mode = mode.to_s.downcase.strip
79
91
  save = mode || !ignore_file.exist?
80
92
 
81
- if mode == :merge
93
+ if mode == 'merge'
94
+ cartage.display('Merging .cartignore...')
82
95
  data = strip_comments_and_empty_lines(ignore_file.readlines)
83
96
 
84
97
  if data.empty?
@@ -87,11 +100,14 @@ or update the Manifest.txt file with the following command:
87
100
  data += strip_comments_and_empty_lines(DEFAULT_IGNORE.split($/))
88
101
  data = data.uniq.join("\n")
89
102
  end
90
- else
103
+ elsif save
104
+ cartage.display('Creating .cartignore...')
91
105
  data = DEFAULT_IGNORE
106
+ else
107
+ cartage.display('.cartignore already exists, skipping...')
92
108
  end
93
109
 
94
- ignore_file.open('w') { |f| f.puts data } if save
110
+ ignore_file.write(data) if save
95
111
  end
96
112
 
97
113
  private
@@ -109,11 +125,8 @@ or update the Manifest.txt file with the following command:
109
125
  end
110
126
 
111
127
  def create_file_list(filename)
112
- Pathname(filename).tap { |file|
113
- file.open('w') { |f|
114
- f.puts prune(%x(git ls-files).split.map(&:chomp)).sort.uniq.join("\n")
115
- }
116
- }
128
+ files = prune(%x(git ls-files).split.map(&:chomp)).sort.uniq.join("\n")
129
+ Pathname(filename).tap { |f| f.write("#{files}\n") }
117
130
  end
118
131
 
119
132
  def ignore_patterns(with_slugignore: false)
@@ -128,10 +141,10 @@ or update the Manifest.txt file with the following command:
128
141
  pats = strip_comments_and_empty_lines(pats)
129
142
 
130
143
  pats.map { |pat|
131
- if pat =~ %r{/\z}
132
- Regexp.new(%r{\A#{pat}})
133
- elsif pat =~ %r{\A/[^*?]+\z}
144
+ if pat =~ %r{\A/[^*?]+\z}
134
145
  Regexp.new(%r{\A#{pat.sub(%r{\A/}, '')}/})
146
+ elsif pat =~ %r{/\z}
147
+ Regexp.new(/\A#{pat}/)
135
148
  else
136
149
  pat
137
150
  end
@@ -154,12 +167,13 @@ or update the Manifest.txt file with the following command:
154
167
  files.reject { |file| prune?(file, exclusions) }
155
168
  end
156
169
 
157
- def prune?(file, exclusions = ignore_patterns())
170
+ def prune?(file, exclusions = ignore_patterns)
158
171
  exclusions.any? do |pat|
159
172
  case pat
160
173
  when /[*?]/
161
- File.fnmatch?(pat, file, File::FNM_PATHNAME | File::FNM_EXTGLOB |
162
- File::FNM_DOTMATCH)
174
+ File.fnmatch?(
175
+ pat, file, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
176
+ )
163
177
  when Regexp
164
178
  file =~ pat
165
179
  else
@@ -220,5 +234,3 @@ tmp/
220
234
  vendor/bundle/
221
235
  EOM
222
236
  end
223
-
224
- require_relative 'manifest/commands'