xmigra 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/xmigra/db_support/mssql.rb +12 -5
- data/lib/xmigra/db_support/psql.rb +12 -2
- data/lib/xmigra/new_migration_adder.rb +63 -0
- data/lib/xmigra/program.rb +23 -2
- data/lib/xmigra/schema_updater.rb +8 -3
- data/lib/xmigra/vcs_support/git.rb +29 -0
- data/lib/xmigra/vcs_support/svn.rb +103 -1
- data/lib/xmigra/version.rb +1 -1
- data/test/git_vcs.rb +29 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f93c4d5e00f442d8bd288980b7d3754b5dca202
|
4
|
+
data.tar.gz: 9485199a710af1bd8f2c0ecbfc5ae9186c5e0097
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d7104989df02da32dc2adb897e9a4abf986b1422f541431080540ea76b101065bf42617536c7b2f7609da19e82917a98e810fc66d89f935366f53da827ca297
|
7
|
+
data.tar.gz: a7a66fd0a47c0dca11235f3c8593981fc4fb4ec89f33ef69c0d08b9aa665abba68d0854fffbc149913a9391d30af79c24af6f6704cc045b045e9acacd49b9e7f
|
@@ -57,7 +57,7 @@ module XMigra
|
|
57
57
|
return @stats_objs
|
58
58
|
end
|
59
59
|
|
60
|
-
def in_ddl_transaction
|
60
|
+
def in_ddl_transaction(options={})
|
61
61
|
parts = []
|
62
62
|
parts << <<-"END_OF_SQL"
|
63
63
|
SET ANSI_NULLS ON
|
@@ -71,18 +71,25 @@ GO
|
|
71
71
|
|
72
72
|
SET NOCOUNT ON
|
73
73
|
GO
|
74
|
-
|
74
|
+
-------------------- ERROR REFERENCE LINE --------------------
|
75
|
+
DECLARE @BATCH_START_OFFSET INTEGER; SET @BATCH_START_OFFSET = 0;
|
75
76
|
BEGIN TRY
|
76
77
|
BEGIN TRAN;
|
77
78
|
END_OF_SQL
|
78
79
|
|
80
|
+
offset_lines = 5
|
79
81
|
each_batch(yield) do |batch|
|
80
82
|
batch_literal = MSSQLSpecifics.string_literal("\n" + batch)
|
81
|
-
parts << "EXEC sp_executesql @statement = #{batch_literal};"
|
83
|
+
parts << "SET @BATCH_START_OFFSET = #{offset_lines}; EXEC sp_executesql @statement = #{batch_literal}; SET @BATCH_START_OFFSET = 0;"
|
84
|
+
offset_lines += parts[-1].count("\n") + 1
|
82
85
|
end
|
83
86
|
|
87
|
+
if options[:dry_run]
|
88
|
+
parts << " PRINT N'Dry-run successful. Rolling back changes.'; ROLLBACK TRAN;"
|
89
|
+
else
|
90
|
+
parts << " COMMIT TRAN;"
|
91
|
+
end
|
84
92
|
parts << <<-"END_OF_SQL"
|
85
|
-
COMMIT TRAN;
|
86
93
|
END TRY
|
87
94
|
BEGIN CATCH
|
88
95
|
ROLLBACK TRAN;
|
@@ -93,7 +100,7 @@ BEGIN CATCH
|
|
93
100
|
|
94
101
|
PRINT N'Update failed: ' + ERROR_MESSAGE();
|
95
102
|
PRINT N' State: ' + CAST(ERROR_STATE() AS NVARCHAR);
|
96
|
-
PRINT N' Line: ' + CAST(ERROR_LINE() AS NVARCHAR)
|
103
|
+
PRINT N' Line: ' + CAST(@BATCH_START_OFFSET + ERROR_LINE() - 1 AS NVARCHAR) + N' after ERROR REFERENCE LINE'
|
97
104
|
|
98
105
|
SELECT
|
99
106
|
@ErrorMessage = N'Update failed: ' + ERROR_MESSAGE(),
|
@@ -23,8 +23,18 @@ module XMigra
|
|
23
23
|
|
24
24
|
def filename_metavariable; "[{filename}]"; end
|
25
25
|
|
26
|
-
def in_ddl_transaction
|
27
|
-
|
26
|
+
def in_ddl_transaction(options={})
|
27
|
+
transaction_wrapup = begin
|
28
|
+
if options[:dry_run]
|
29
|
+
PgSQLSpecifics.in_plpgsql(%Q{
|
30
|
+
RAISE NOTICE 'Dry-run successful. Rolling back changes.';
|
31
|
+
}) + "\nROLLBACK;"
|
32
|
+
else
|
33
|
+
"COMMIT;"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
["BEGIN;", yield, transaction_wrapup].join("\n")
|
28
38
|
end
|
29
39
|
|
30
40
|
def check_execution_environment_sql
|
@@ -5,6 +5,11 @@ module XMigra
|
|
5
5
|
class NewMigrationAdder < SchemaManipulator
|
6
6
|
OBSOLETE_VERINC_FILE = 'version-upgrade-obsolete.yaml'
|
7
7
|
|
8
|
+
# Return this class (not an instance of it) from a Proc yielded by
|
9
|
+
# each_possible_production_chain_extension_handler to continue through the
|
10
|
+
# handler chain.
|
11
|
+
class IgnoreHandler; end
|
12
|
+
|
8
13
|
def initialize(path)
|
9
14
|
super(path)
|
10
15
|
end
|
@@ -23,6 +28,13 @@ module XMigra
|
|
23
28
|
end
|
24
29
|
Hash === head_info or raise XMigra::Error, "Invalid #{MigrationChain::HEAD_FILE} format"
|
25
30
|
|
31
|
+
if !head_info.empty? && respond_to?(:vcs_production_contents) && (production_head_contents = vcs_production_contents(head_file))
|
32
|
+
production_head_info = YAML.load(production_head_contents)
|
33
|
+
extending_production = head_info[MigrationChain::LATEST_CHANGE] == production_head_info[MigrationChain::LATEST_CHANGE]
|
34
|
+
else
|
35
|
+
extending_production = false
|
36
|
+
end
|
37
|
+
|
26
38
|
new_fpath = struct_dir.join(
|
27
39
|
[Date.today.strftime("%Y-%m-%d"), summary].join(' ') + '.yaml'
|
28
40
|
)
|
@@ -68,7 +80,58 @@ module XMigra
|
|
68
80
|
mv_method.call(bufp, obufp)
|
69
81
|
end
|
70
82
|
|
83
|
+
production_chain_extended if extending_production
|
84
|
+
|
71
85
|
return new_fpath
|
72
86
|
end
|
87
|
+
|
88
|
+
# Called when the chain of migrations in the production/master branch is
|
89
|
+
# extended with a new migration.
|
90
|
+
#
|
91
|
+
# This method calls each_possible_production_chain_extension_handler to
|
92
|
+
# generate a chain of handlers.
|
93
|
+
#
|
94
|
+
def production_chain_extended
|
95
|
+
Dir.chdir(self.path) do
|
96
|
+
each_possible_production_chain_extension_handler do |handler|
|
97
|
+
if handler.kind_of? Proc
|
98
|
+
handler_result = handler[]
|
99
|
+
break true unless handler_result == IgnoreHandler
|
100
|
+
else
|
101
|
+
handler_result = system(handler)
|
102
|
+
break true unless handler_result.nil?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Yield command strings or Proc instances to attempt handling the
|
109
|
+
# production-chain-extension event
|
110
|
+
#
|
111
|
+
# Strings yielded by this method will be executed using Kernel#system. If
|
112
|
+
# this results in `nil` (command does not exist), processing will continue
|
113
|
+
# through the remaining handlers.
|
114
|
+
#
|
115
|
+
# Procs yielded by this method will be executed without any parameters.
|
116
|
+
# Unless the invocation returns IgnoreHandler, event processing will
|
117
|
+
# terminate after invocation.
|
118
|
+
#
|
119
|
+
def each_possible_production_chain_extension_handler
|
120
|
+
yield "on-prod-chain-extended-local"
|
121
|
+
if respond_to?(:vcs_prod_chain_extension_handler) && (vcs_handler = vcs_prod_chain_extension_handler)
|
122
|
+
yield vcs_handler.to_s
|
123
|
+
end
|
124
|
+
yield "on-prod-chain-extended"
|
125
|
+
yield Proc.new {
|
126
|
+
next unless respond_to? :warning
|
127
|
+
warning(<<END_MESSAGE)
|
128
|
+
This command has extended the production migration chain.
|
129
|
+
|
130
|
+
Backing up your development database now may be advantageous in case you need
|
131
|
+
to accept migrations developed in parallel from upstream before merging your
|
132
|
+
work back to the mainline.
|
133
|
+
END_MESSAGE
|
134
|
+
}
|
135
|
+
end
|
73
136
|
end
|
74
137
|
end
|
data/lib/xmigra/program.rb
CHANGED
@@ -161,6 +161,14 @@ END_OF_HELP
|
|
161
161
|
end
|
162
162
|
end
|
163
163
|
|
164
|
+
if use[:dry_run]
|
165
|
+
options.dry_run = false
|
166
|
+
flags.on("--dry-run", "Generated script will test upgrade",
|
167
|
+
" commands without committing changes") do
|
168
|
+
options.dry_run = true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
164
172
|
options.source_dir = Dir.pwd
|
165
173
|
flags.on("--source=DIR", "Work from/on the schema in DIR") do |dir|
|
166
174
|
options.source_dir = File.expand_path(dir)
|
@@ -349,7 +357,9 @@ migration, and is included in the upgrade metadata stored in the database. Use
|
|
349
357
|
of the '%program_cmd new' command is recommended; it handles several tiresome
|
350
358
|
and error-prone tasks: creating a new migration file with a conformant name,
|
351
359
|
setting the "starting from" section to the correct value, and updating
|
352
|
-
SCHEMA/structure/head.yaml to reference the newly generated file.
|
360
|
+
SCHEMA/structure/head.yaml to reference the newly generated file. It can also
|
361
|
+
print a warning or run a command when the source condition indicates a database
|
362
|
+
backup would be a good idea.
|
353
363
|
|
354
364
|
The SCHEMA/structure/head.yaml file deserves special note: it contains a
|
355
365
|
reference to the last migration to be applied. Because of this, parallel
|
@@ -672,6 +682,16 @@ This command generates a new migration file and ties it into the current
|
|
672
682
|
migration chain. The name of the new file is generated from today's date and
|
673
683
|
the given MIGRATION_SUMMARY. The resulting new file may be opened in an
|
674
684
|
editor (see the --[no-]edit option).
|
685
|
+
|
686
|
+
If this command extends the production migration chain, it will attempt to
|
687
|
+
locate a handler function ("on-prod-chain-extended-local", a VCS-specified
|
688
|
+
handler, or "on-prod-chain-extended") executable from the schema root
|
689
|
+
directory. If it cannot locate one of these, it will print a message about
|
690
|
+
the advisability of taking a backup of the development database.
|
691
|
+
|
692
|
+
Git VCS specifics: The #{GitSpecifics::PRODUCTION_CHAIN_EXTENSION_COMMAND} attribute on the
|
693
|
+
database.yaml file can be used to specify a handler for the production chain
|
694
|
+
extension.
|
675
695
|
END_OF_HELP
|
676
696
|
|
677
697
|
argument_error_unless(args.length == 1,
|
@@ -689,7 +709,7 @@ END_OF_HELP
|
|
689
709
|
end
|
690
710
|
|
691
711
|
subcommand 'upgrade', "Generate an upgrade script" do |argv|
|
692
|
-
args, options = command_line(argv, {:production=>true, :outfile=>true},
|
712
|
+
args, options = command_line(argv, {:production=>true, :outfile=>true, :dry_run=>true},
|
693
713
|
:help=> <<END_OF_HELP)
|
694
714
|
Running this command will generate an update script from the source schema.
|
695
715
|
Generation of a production script involves more checks on the status of the
|
@@ -705,6 +725,7 @@ END_OF_HELP
|
|
705
725
|
sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
|
706
726
|
sql_gen.load_plugin!
|
707
727
|
sql_gen.production = options.production
|
728
|
+
sql_gen.dry_run = options.dry_run
|
708
729
|
|
709
730
|
output_to(options.outfile) do |out_stream|
|
710
731
|
out_stream.print(sql_gen.update_sql)
|
@@ -50,16 +50,21 @@ RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
|
|
50
50
|
end
|
51
51
|
|
52
52
|
@production = false
|
53
|
+
@dry_run = false
|
53
54
|
end
|
54
55
|
|
55
|
-
attr_accessor :production
|
56
|
+
attr_accessor :production, :dry_run
|
56
57
|
attr_reader :migrations, :access_artifacts, :indexes, :branch_upgrade
|
57
58
|
|
58
59
|
def inspect
|
59
60
|
"<#{self.class.name}: path=#{path.to_s.inspect}, db=#{@db_specifics}, vcs=#{@vcs_specifics}>"
|
60
61
|
end
|
61
62
|
|
62
|
-
def in_ddl_transaction
|
63
|
+
def in_ddl_transaction(options = {})
|
64
|
+
if options[:dry_run]
|
65
|
+
raise(XMigra::Error, 'DDL transaction not supported; dry-run unavailable.')
|
66
|
+
end
|
67
|
+
|
63
68
|
yield
|
64
69
|
end
|
65
70
|
|
@@ -90,7 +95,7 @@ RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
|
|
90
95
|
intro_comment << "\n\n"
|
91
96
|
|
92
97
|
# If supported, wrap transactionality around modifications
|
93
|
-
intro_comment + in_ddl_transaction do
|
98
|
+
intro_comment + in_ddl_transaction(:dry_run => @dry_run) do
|
94
99
|
script_parts = [
|
95
100
|
# Check for blatantly incorrect application of script, e.g. running
|
96
101
|
# on master or template database.
|
@@ -6,6 +6,7 @@ module XMigra
|
|
6
6
|
|
7
7
|
MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
|
8
8
|
MASTER_BRANCH_SUBDIR = 'xmigra-master'
|
9
|
+
PRODUCTION_CHAIN_EXTENSION_COMMAND = 'xmigra-on-production-chain-extended'
|
9
10
|
|
10
11
|
class AttributesFile
|
11
12
|
def initialize(effect_root, access=:shared)
|
@@ -244,6 +245,34 @@ module XMigra
|
|
244
245
|
git(:rm, path, :get_result=>false)
|
245
246
|
end
|
246
247
|
|
248
|
+
def vcs_production_contents(path)
|
249
|
+
return nil unless git_master_head(:required => false)
|
250
|
+
git_fetch_master_branch
|
251
|
+
git(:show, [git_master_local_branch, git_internal_path].join(':'), :quiet=>true)
|
252
|
+
rescue VersionControlError
|
253
|
+
return nil
|
254
|
+
end
|
255
|
+
|
256
|
+
def vcs_prod_chain_extension_handler
|
257
|
+
attr_val = GitSpecifics.attr_values(
|
258
|
+
PRODUCTION_CHAIN_EXTENSION_COMMAND,
|
259
|
+
self.path + SchemaManipulator::DBINFO_FILE,
|
260
|
+
:required=>false,
|
261
|
+
)[0]
|
262
|
+
|
263
|
+
# Check for special value
|
264
|
+
return nil if attr_val == 'unspecified'
|
265
|
+
|
266
|
+
handler_path = Pathname(attr_val)
|
267
|
+
if handler_path.absolute?
|
268
|
+
return handler_path if handler_path.exist?
|
269
|
+
else
|
270
|
+
handler_path = self.path + handler_path
|
271
|
+
return handler_path if handler_path.exist?
|
272
|
+
end
|
273
|
+
return attr_val
|
274
|
+
end
|
275
|
+
|
247
276
|
def production_pattern
|
248
277
|
".+"
|
249
278
|
end
|
@@ -22,7 +22,7 @@ module XMigra
|
|
22
22
|
def run_svn(subcmd, *args)
|
23
23
|
options = (Hash === args[-1]) ? args.pop : {}
|
24
24
|
no_result = !options.fetch(:get_result, true)
|
25
|
-
raw_result = options.fetch(:raw, false)
|
25
|
+
raw_result = options.fetch(:raw, false) || subcmd.to_s == 'cat'
|
26
26
|
|
27
27
|
cmd_parts = ["svn", subcmd.to_s]
|
28
28
|
cmd_parts << "--xml" unless no_result || raw_result
|
@@ -235,5 +235,107 @@ END_OF_MESSAGE
|
|
235
235
|
return @subversion_info if defined? @subversion_info
|
236
236
|
return @subversion_info = subversion(:info, self.path)
|
237
237
|
end
|
238
|
+
|
239
|
+
def vcs_production_contents(path)
|
240
|
+
path = Pathname(path)
|
241
|
+
|
242
|
+
# Check for a production pattern. If none exists, there is no way to
|
243
|
+
# identify which branches are production, so essentially no production
|
244
|
+
# content:
|
245
|
+
prod_pat = self.production_pattern
|
246
|
+
return nil if prod_pat.nil?
|
247
|
+
prod_pat = Regexp.compile(prod_pat.chomp)
|
248
|
+
|
249
|
+
# Is the current branch a production branch? If so, cat the committed
|
250
|
+
# version:
|
251
|
+
if branch_identifier =~ prod_pat
|
252
|
+
return svn(:cat, path.to_s)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Use an SvnHistoryTracer to walk back through the history of self.path
|
256
|
+
# looking for a copy from a production branch.
|
257
|
+
tracer = SvnHistoryTracer.new(self.path)
|
258
|
+
|
259
|
+
while !(match = tracer.earliest_loaded_repopath =~ prod_pat) && tracer.load_parent_commit
|
260
|
+
# loop
|
261
|
+
end
|
262
|
+
|
263
|
+
if match
|
264
|
+
subversion(:cat, "-r#{tracer.earliest_loaded_revision}", path.to_s)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class SvnHistoryTracer
|
270
|
+
include SubversionSpecifics
|
271
|
+
|
272
|
+
def initialize(path)
|
273
|
+
@path = Pathname(path)
|
274
|
+
info_doc = subversion(:info, path.to_s)
|
275
|
+
@root_url = info_doc.elements['string(info/entry/repository/root)']
|
276
|
+
@most_recent_commit = info_doc.elements['string(info/entry/@revision)'].to_i
|
277
|
+
@history = []
|
278
|
+
@next_query = [branch_identifier, @most_recent_commit]
|
279
|
+
@history.unshift(@next_query.dup)
|
280
|
+
end
|
281
|
+
|
282
|
+
attr_reader :path, :most_recent_commit, :history
|
283
|
+
|
284
|
+
def load_parent_commit
|
285
|
+
log_doc = next_earlier_log
|
286
|
+
if copy_elt = copying_element(log_doc)
|
287
|
+
trailing_part = branch_identifier[copy_elt.text.length..-1]
|
288
|
+
@next_query = [
|
289
|
+
copy_elt.attributes['copyfrom-path'] + trailing_part,
|
290
|
+
copy_elt.attributes['copyfrom-rev'].to_i
|
291
|
+
]
|
292
|
+
@history.unshift(@next_query)
|
293
|
+
@next_query.dup
|
294
|
+
elsif change_elt = log_doc.elements['/log/logentry']
|
295
|
+
@next_query[1] = change_elt.attributes['revision'].to_i - 1
|
296
|
+
@next_query.dup if @next_query[1] > 0
|
297
|
+
else
|
298
|
+
@next_query[1] -= 1
|
299
|
+
@next_query.dup if @next_query[1] > 0
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def history_exhausted?
|
304
|
+
@next_query[1] <= 0
|
305
|
+
end
|
306
|
+
|
307
|
+
def earliest_loaded_repopath
|
308
|
+
history[0][0]
|
309
|
+
end
|
310
|
+
|
311
|
+
def earliest_loaded_url
|
312
|
+
@root_url + history[0][0]
|
313
|
+
end
|
314
|
+
|
315
|
+
def earliest_loaded_revision
|
316
|
+
history[0][1]
|
317
|
+
end
|
318
|
+
|
319
|
+
def earliest_loaded_pinned_url(rel_path=nil)
|
320
|
+
pin_rev = @history[0][1]
|
321
|
+
if rel_path.nil?
|
322
|
+
[earliest_loaded_url, pin_rev.to_s].join('@')
|
323
|
+
else
|
324
|
+
rel_path = Pathname(rel_path)
|
325
|
+
"#{earliest_loaded_url}/#{rel_path}@#{pin_rev}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def copying_element(log_doc)
|
330
|
+
log_doc.each_element %Q{/log/logentry/paths/path[@copyfrom-path]} do |elt|
|
331
|
+
return elt if elt.text == @next_query[0]
|
332
|
+
return elt if @next_query[0].start_with? (elt.text + '/')
|
333
|
+
end
|
334
|
+
return nil
|
335
|
+
end
|
336
|
+
|
337
|
+
def next_earlier_log
|
338
|
+
subversion(:log, '-l1', '-v', "-r#{@next_query[1]}:1", self.path)
|
339
|
+
end
|
238
340
|
end
|
239
341
|
end
|
data/lib/xmigra/version.rb
CHANGED
data/test/git_vcs.rb
CHANGED
@@ -239,4 +239,33 @@ if GIT_PRESENT
|
|
239
239
|
end
|
240
240
|
end
|
241
241
|
end
|
242
|
+
|
243
|
+
run_test "XMigra detects extension of the production migration chain" do
|
244
|
+
capture_chain_extension = Module.new do
|
245
|
+
def production_chain_extended(*args)
|
246
|
+
(@captured_call_args ||= []) << args
|
247
|
+
end
|
248
|
+
|
249
|
+
attr_reader :captured_call_args
|
250
|
+
end
|
251
|
+
|
252
|
+
2.temp_dirs do |upstream, repo|
|
253
|
+
initialize_git_repo(upstream)
|
254
|
+
|
255
|
+
Dir.chdir(upstream) do
|
256
|
+
commit_a_migration "first table"
|
257
|
+
make_this_branch_master
|
258
|
+
end
|
259
|
+
|
260
|
+
`git clone "#{upstream.expand_path}" "#{repo}" 2>/dev/null`
|
261
|
+
|
262
|
+
Dir.chdir(repo) do
|
263
|
+
XMigra::NewMigrationAdder.new('.') do |tool|
|
264
|
+
tool.extend(capture_chain_extension)
|
265
|
+
tool.add_migration('Create foo table')
|
266
|
+
assert_eq tool.captured_call_args, [[]]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
242
271
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xmigra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Next IT Corporation
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-07-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|