palletjack 0.1.2

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.
@@ -0,0 +1,264 @@
1
+ class PalletJack
2
+ class KeyTransformer
3
+ class Reader < KeyTransformer
4
+ def concatenate(param, context = {})
5
+ context[:value].split(param) if context[:value]
6
+ end
7
+ end
8
+
9
+ class Writer < KeyTransformer
10
+ def concatenate(param, context = {})
11
+ context[:value].join(param) if context[:value]
12
+ end
13
+
14
+ # Internal synthesize* helper method
15
+ # N.B. rdoc will not be generated, because method is private.
16
+ #
17
+ # :call-seq:
18
+ # synthesize_internal(param, dictionary) -> string or nil
19
+ #
20
+ # Use the single +String+ or +Enumerable+ containing +String+
21
+ # in +param+ to build and return a substitution value. If any
22
+ # failure occurs while building the new value, return +nil+.
23
+ #
24
+ # Substitutions are made from key-value pairs in +dictionary+
25
+ #
26
+ # YAML structure:
27
+ #
28
+ # - some_rule: "rule"
29
+ #
30
+ # or
31
+ #
32
+ # - some_rule:
33
+ # - "rule"
34
+ # - "rule"
35
+ # ...
36
+ #
37
+ # Rules are strings used to build the new value. The value of
38
+ # another key is inserted by <tt>#[key]</tt>, and all other
39
+ # characters are copied verbatim.
40
+ #
41
+ # Rules are evaluated in order, and the first one to
42
+ # successfully produce a value without failing a key lookup is
43
+ # used.
44
+ #
45
+
46
+ def synthesize_internal(param, dictionary, result=String.new)
47
+ case param
48
+ when String
49
+ rex=/#\[([[:alnum:]._-]+)\]/
50
+ if md=rex.match(param) then
51
+ result << md.pre_match
52
+ return unless lookup = dictionary[md[1]]
53
+ result << lookup.to_s
54
+ synthesize_internal(md.post_match, dictionary, result)
55
+ else
56
+ result << param
57
+ end
58
+ else # Enumerable
59
+ param.reduce(false) do |found_one, alternative|
60
+ found_one || synthesize_internal(alternative, dictionary)
61
+ end
62
+ end
63
+ end
64
+ private :synthesize_internal
65
+
66
+ # Synthesize a pallet value by pasting others together.
67
+ #
68
+ # :call-seq:
69
+ # synthesize(param, context) -> string or nil
70
+ #
71
+ # If +context+ contains a non-nil +:value+, an earlier transform
72
+ # has already produced a value for this key, so do nothing and
73
+ # return +nil+.
74
+ #
75
+ # Otherwise, use the parsed YAML structure in +param+ to build
76
+ # and return a new value. If any failure occurs while building
77
+ # the new value, return +nil+ to let another transform try.
78
+ #
79
+ # YAML structure:
80
+ #
81
+ # - synthesize: "rule"
82
+ #
83
+ # or
84
+ #
85
+ # - synthesize:
86
+ # - "rule"
87
+ # - "rule"
88
+ # ...
89
+ #
90
+ # Rules are strings used to build the new value. The value of
91
+ # another key is inserted by <tt>#[key]</tt>, and all other
92
+ # characters are copied verbatim.
93
+ #
94
+ # Rules are evaluated in order, and the first one to
95
+ # successfully produce a value without failing a key lookup is
96
+ # used.
97
+ #
98
+ # Example:
99
+ #
100
+ # - net.dns.fqdn:
101
+ # - synthesize: "#[net.ip.name].#[domain.name]"
102
+ #
103
+ # - chassis.nic.name:
104
+ # - synthesize:
105
+ # - "p#[chassis.nic.pcislot]p#[chassis.nic.port]"
106
+ # - "em#[chassis.nic.port]"
107
+
108
+ def synthesize(param, context = {})
109
+ return if context[:value]
110
+
111
+ synthesize_internal(param, context[:pallet])
112
+ end
113
+
114
+ # Synthesize a pallet value from others, using regular
115
+ # expressions to pull out parts of values.
116
+ #
117
+ # :call-seq:
118
+ # synthesize_regexp(param, context) -> string or nil
119
+ #
120
+ # If +context+ contains a non-nil +:value+, an earlier transform
121
+ # has already produced a value for this key, so do nothing and
122
+ # return +nil+.
123
+ #
124
+ # Otherwise, use the parsed YAML structure in +param+ to build
125
+ # and return a new value. If any failure occurs while building
126
+ # the new value, return +nil+ to let another transform try.
127
+ #
128
+ # YAML structure:
129
+ #
130
+ # - synthesize_regexp:
131
+ # sources:
132
+ # source0:
133
+ # key: "key"
134
+ # regexp: "regexp"
135
+ # source1:
136
+ # key: "key"
137
+ # regexp: "regexp"
138
+ # ...
139
+ # produce: "recipe"
140
+ #
141
+ # where:
142
+ # [+sourceN+] Arbitrary number of sources for partial values,
143
+ # with arbitrary names
144
+ # [+key+] Name of the key to read a partial value from
145
+ # [+regexp+] Regular expression for parsing the value indicated
146
+ # by +key+, with named captures used to save
147
+ # substrings for producing the final value. Capture
148
+ # names must not be repeated within the same
149
+ # synthesize_regexp block.
150
+ # [+produce+] A recipe for building the new value. Named
151
+ # captures are inserted by <tt>#[capture]</tt>, and
152
+ # all other characters are copied verbatim.
153
+ #
154
+ # Example:
155
+ #
156
+ # Take strings like +192.168.0.0_24+ from +pallet.ipv4_network+
157
+ # and produce strings like +192.168.0.0/24+ in +net.ipv4.cidr+.
158
+ #
159
+ # - net.ipv4.cidr:
160
+ # - synthesize_regexp:
161
+ # sources:
162
+ # ipv4_network:
163
+ # key: "pallet.ipv4_network"
164
+ # regexp: "^(?<network>[0-9.]+)_(?<prefix_length>[0-9]+)$"
165
+ # produce: "#[network]/#[prefix_length]"
166
+
167
+ def synthesize_regexp(param, context = {})
168
+ return if context[:value]
169
+
170
+ captures = {}
171
+
172
+ param["sources"].each do |_, source|
173
+ # Trying to read values from a non-existent key. Return nil
174
+ # and let another transform try.
175
+ return unless lookup = context[:pallet][source["key"]]
176
+
177
+ # Save all named captures
178
+ Regexp.new(source["regexp"]).match(lookup) do |md|
179
+ md.names.each do |name|
180
+ captures[name] = md[name.to_sym]
181
+ end
182
+ end
183
+ end
184
+
185
+ synthesize_internal(param["produce"], captures)
186
+ end
187
+
188
+ # Synthesized value will override an inherited value for a
189
+ # +key+, but in some cases the intent is actually to only
190
+ # synthesize a value when there is no inherited value. This
191
+ # provides early termination of transforms for such keys.
192
+ #
193
+ # Example:
194
+ #
195
+ # - net.layer2.name:
196
+ # - inherit: ~
197
+ # - synthesize: "#[chassis.nic.name]"
198
+
199
+ def inherit(_, context = {})
200
+ throw context[:abort] if context[:pallet][context[:key]]
201
+ end
202
+ end
203
+
204
+ def initialize(key_transforms={})
205
+ @key_transforms = key_transforms
206
+ end
207
+
208
+ # Destructively transform the values in +pallet+ according to the
209
+ # loaded transform rules.
210
+ #
211
+ # YAML structure:
212
+ #
213
+ # - key:
214
+ # - transform1:
215
+ # [transform-specific configuration]
216
+ # - transform2:
217
+ # [transform-specific configuration]
218
+ # [...]
219
+ #
220
+ # Transforms are evaluated in order from top to bottom, and the
221
+ # first one to successfully produce a value is used.
222
+ #
223
+ # Transforms are methods in PalletJack::KeyTransformer::Writer,
224
+ # called by name. They should return the new value, or +false+ if
225
+ # unsuccessful.
226
+ #
227
+ # Transforms are given two parameters, +param+ and +context+:
228
+ # [+param+] transform-specific configuration from transforms.yaml
229
+ # [+context+]
230
+ # [+pallet+] The pallet object being processed
231
+ # [+key+] The key from transforms.yaml being processed
232
+ # [+value+] Current locally assigned value for key in pallet
233
+ # [+abort+] #throw this to abort transforms for current key
234
+
235
+ def transform!(pallet)
236
+ @key_transforms.each do |keytrans_item|
237
+
238
+ # Enable early termination of transforms for a key
239
+ # by wrapping execution in a catch block.
240
+ catch do |abort_tag|
241
+ key, transforms = keytrans_item.flatten
242
+ context = {
243
+ pallet: pallet,
244
+ key: key,
245
+ value: pallet[key, shallow: true],
246
+ abort: abort_tag
247
+ }
248
+
249
+ transforms.each do |t|
250
+ transform, param = t.flatten
251
+ if self.respond_to?(transform.to_sym) then
252
+ if new_value = self.send(transform.to_sym, param, context)
253
+ then
254
+ pallet[key] = new_value
255
+ break
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ @hash
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,80 @@
1
+ class PalletJack
2
+ # PalletJack managed pallet of key boxes inside a warehouse.
3
+ class Pallet < KVDAG::Vertex
4
+
5
+ attr_reader :name
6
+ attr_reader :kind
7
+
8
+ # N.B: A pallet should never be created manually; use
9
+ # +PalletJack::new+ to initialize a complete warehouse.
10
+ #
11
+ # [+jack+] PalletJack that will manage this pallet.
12
+ # [+path+] Filesystem path to pallet data.
13
+ #
14
+ # Create PalletJack managed singletonish pallet.
15
+ #
16
+ # Use relative path inside of warehouse as kind/name for this
17
+ # pallet, and make a singletonish object for that key.
18
+
19
+ def Pallet.new(jack, path) #:doc:
20
+ ppath, name = File.split(path)
21
+ _, kind = File.split(ppath)
22
+
23
+ jack.pallets[kind] ||= Hash.new
24
+ jack.pallets[kind][name] || super
25
+ end
26
+
27
+ # N.B: A pallet should never be created manually; use
28
+ # +PalletJack::new+ to initialize a complete warehouse.
29
+ #
30
+ # [+jack+] PalletJack that will manage this pallet.
31
+ # [+path+] Filesystem path to pallet data.
32
+ #
33
+ # Loads and merges all YAML files in +path+ into this Vertex.
34
+ #
35
+ # Follows all symlinks in +path+ and creates edges towards
36
+ # the pallet located in the symlink target.
37
+
38
+ private :initialize
39
+ def initialize(jack, path) #:notnew:
40
+ @jack = jack
41
+ @path = path
42
+ ppath, @name = File.split(path)
43
+ _, @kind = File.split(ppath)
44
+ boxes = Array.new
45
+
46
+ super(jack, pallet:{@kind => @name})
47
+
48
+ Dir.foreach(path) do |file|
49
+ next if file[0] == '.'
50
+ filepath = File.join(path, file)
51
+ filestat = File.lstat(filepath)
52
+ case
53
+ when (filestat.file? and file =~ /\.yaml$/)
54
+ merge!(YAML::load_file(filepath))
55
+ boxes << file
56
+ when filestat.symlink?
57
+ link = File.readlink(filepath)
58
+ _, lname = File.split(link)
59
+
60
+ pallet = Pallet.new(jack, File.absolute_path(link, path))
61
+ edge(pallet, pallet:{references:{file => lname}})
62
+ end
63
+ end
64
+ merge!(pallet:{boxes: boxes})
65
+ @jack.keytrans_writer.transform!(self)
66
+ @jack.pallets[@kind][@name] = self
67
+ end
68
+
69
+ def inspect
70
+ "#<%s:%x>" % [self.class, self.object_id, @path]
71
+ end
72
+
73
+ # Override standard to_yaml serialization, because pallet objects
74
+ # are ephemeral by nature. The natural serialization is that of
75
+ # their to_hash analogue.
76
+ def to_yaml
77
+ to_hash.to_yaml
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,463 @@
1
+ require 'palletjack'
2
+ require 'fileutils'
3
+ require 'optparse'
4
+ require 'singleton'
5
+ require 'rugged'
6
+
7
+ class PalletJack
8
+
9
+ # Superclass for PalletJack tool implementations
10
+ #
11
+ # Provides convenience methods for option parsing, file generation,
12
+ # and warehouse structure management.
13
+ #
14
+ # Example:
15
+ # require 'palletjack/tool'
16
+ #
17
+ # class MyTool < PalletJack::Tool
18
+ # def parse_options(parser)
19
+ # parser.on('-o DIR', '--output DIR',
20
+ # 'output directory',
21
+ # String) {|dir| options[:output] = dir }
22
+ #
23
+ # required_option :output
24
+ # end
25
+ #
26
+ # attr_reader :state
27
+ #
28
+ # def process
29
+ # @state = {}
30
+ # jack.each(kind:'system') do |sys|
31
+ # @state[sys.name] = sys
32
+ # end
33
+ # end
34
+ #
35
+ # def output
36
+ # @state.each do |name, data|
37
+ # config_dir :output, name
38
+ # config_file :output, name, "dump.yaml" do |file|
39
+ # file << data.to_yaml
40
+ # end
41
+ # end
42
+ # end
43
+ # end
44
+ #
45
+ # if __FILE__ == $0
46
+ # MyTool.run
47
+ # end
48
+
49
+ class Tool
50
+ include Singleton
51
+
52
+ # v0.1.1 API:
53
+ #
54
+ # :call-seq:
55
+ # run
56
+ #
57
+ # Main tool framework driver.
58
+ #
59
+ # Run the entire tool; setup, process and output. Actual tools
60
+ # will want to use this function, while testing and other
61
+ # activities that require poking around in internal state will
62
+ # want to run the partial functions instead.
63
+ #
64
+ # Example:
65
+ #
66
+ # if MyTool.standalone?(__FILE__)
67
+ # MyTool.run
68
+ # end
69
+
70
+ # v0.1.0 API, retained until all tools have been updated to
71
+ # v0.1.1:
72
+ #
73
+ # :call-seq:
74
+ # run &block
75
+ #
76
+ # Run the +block+ given in the context of the tool singleton instance
77
+ # as convenience for simple tools.
78
+ #
79
+ # More complex tools probably want to override parse_options to
80
+ # add option parsing, and split functionality into multiple methods.
81
+ #
82
+ # Example:
83
+ #
84
+ # MyTool.run { jack.each(kind:'system') {|sys| puts sys.to_yaml } }
85
+
86
+ def self.run(&block)
87
+ if block
88
+ # v0.1.0 API. When all tools have been ported, remove this and
89
+ # bump the minor version number.
90
+ instance.setup
91
+ instance.instance_eval(&block)
92
+ else
93
+ # v0.1.1 API
94
+ instance.setup
95
+ instance.process
96
+ instance.output
97
+ end
98
+ end
99
+
100
+ # Predicate for detecting if we are being invoked as a standalone
101
+ # tool, or loaded by e.g. a test framework.
102
+
103
+ def self.standalone?(file)
104
+ File::basename(file) == File::basename($0)
105
+ end
106
+
107
+ # Generate data in an internal format, saving it for later testing
108
+ # or writing to disk by #output.
109
+ #
110
+ # Override this function in specific tool classes.
111
+ #
112
+ # Example:
113
+ #
114
+ # class MyTool < PalletJack::Tool
115
+ # def process
116
+ # @systems = Set.new
117
+ # jack.each(kind:'system') do |s|
118
+ # @systems << s
119
+ # end
120
+ # end
121
+ # end
122
+
123
+ def process
124
+ end
125
+
126
+ # Output data in its final format, probably to disk or stdout.
127
+ #
128
+ # Example:
129
+ #
130
+ # class MyTool < PalletJack::Tool
131
+ # def output
132
+ # @systems.each do |s|
133
+ # puts s.name
134
+ # end
135
+ # end
136
+ # end
137
+
138
+ def output
139
+ end
140
+
141
+ # Return the command line argument list to be used. Replace this
142
+ # method when testing.
143
+
144
+ def argv
145
+ ARGV
146
+ end
147
+
148
+ # Set up the singleton instance
149
+ #
150
+ # Default setup will add options for --warehouse and --help to the
151
+ # OptionParser, and set the banner to something useful.
152
+ #
153
+ # Any exceptions raised during option parsing will abort execution
154
+ # with usage information.
155
+
156
+ def setup
157
+ @parser = OptionParser.new
158
+ @options = {}
159
+ @option_checks = []
160
+
161
+ @parser.banner = "Usage: #{$PROGRAM_NAME} -w <warehouse> [options]"
162
+ @parser.separator ''
163
+ @parser.on('-w DIR', '--warehouse DIR',
164
+ 'warehouse directory', String) {|dir|
165
+ @options[:warehouse] = dir }
166
+ @parser.on_tail('-h', '--help', 'display usage information') {
167
+ raise ArgumentError }
168
+
169
+ parse_options(@parser)
170
+
171
+ @parser.parse!(argv)
172
+ @option_checks.each {|check| check.call }
173
+ rescue
174
+ abort(usage)
175
+ end
176
+
177
+ # Additional option parsing
178
+ #
179
+ # The default instance initalization will add option parsing for
180
+ # <tt>-w</tt>/<tt>--warehouse</tt> and <tt>-h</tt>/<tt>--help</tt>,
181
+ # and a simple banner string.
182
+ #
183
+ # Implementations needing more options than the default, a more
184
+ # informative banner, or requirement checks for parsed options should
185
+ # override this empty method.
186
+ #
187
+ # Any exceptions raised will abort execution with usage information.
188
+ #
189
+ # Example:
190
+ #
191
+ # class MyTool < PalletJack::Tool
192
+ # def parse_options(parser)
193
+ # parser.on('-o DIR', '--output DIR',
194
+ # 'output directory',
195
+ # String) {|dir| options[:output] = dir }
196
+ #
197
+ # required_option :output
198
+ # end
199
+ # end
200
+
201
+ def parse_options(parser)
202
+ end
203
+
204
+ # Require the presence of one of the given options.
205
+ #
206
+ # Must not be called outside the scope of the parse_options method.
207
+ #
208
+ # Raises ArgumentError if none exist in options[]
209
+ #
210
+ # Example:
211
+ #
212
+ # def parse_options(parser)
213
+ # ...
214
+ # required_option :output
215
+ # end
216
+
217
+ def required_option(*opts)
218
+ @option_checks << lambda do
219
+ raise ArgumentError unless opts.any? {|opt| options[opt]}
220
+ end
221
+ end
222
+
223
+ # Require the presence of no more than one of the given options.
224
+ #
225
+ # Must not be called outside the scope of the parse_options method.
226
+ #
227
+ # Raises ArgumentError if more than one exist in options[]
228
+ #
229
+ # Example:
230
+ #
231
+ # def parse_options(parse)
232
+ # ...
233
+ # required_option :output_file, :output_stdout
234
+ # exclusive_options :output_file, :output_stdout
235
+ # end
236
+
237
+ def exclusive_options(*opts)
238
+ @option_checks << lambda do
239
+ raise ArgumentError if opts.count {|opt| options[opt]} > 1
240
+ end
241
+ end
242
+
243
+ # Usage information from option parser
244
+ #
245
+ # Example:
246
+ #
247
+ # abort(usage) unless options[:warehouse]
248
+
249
+ def usage
250
+ @parser.to_s
251
+ end
252
+
253
+ # Hash containing all parsed options.
254
+
255
+ attr_reader :options
256
+
257
+ # Pallet containing all warehouse defined configuration options
258
+ #
259
+ # Configuration options for tools can be stored as pallets in
260
+ # the warehouse:
261
+ #
262
+ # _config
263
+ # |
264
+ # +-- MyTool
265
+ # | |
266
+ # | `-- somecfg.yaml
267
+
268
+ def config
269
+ @config ||= jack.fetch(kind:'_config',
270
+ name: self.class.to_s) rescue Hash.new
271
+ end
272
+
273
+ # Return the PalletJack object for <tt>--warehouse</tt>
274
+ # Aborts execution with usage message if the warehouse was
275
+ # not specified.
276
+
277
+ def jack
278
+ abort(usage) unless options[:warehouse]
279
+ @jack ||= PalletJack.load(options[:warehouse])
280
+ end
281
+
282
+ # Build a filesystem path from path components
283
+ #
284
+ # Symbols are looked up in the options dictionary.
285
+ # All other types are converted to strings. The
286
+ # resulting list is fed to File#join to produce a
287
+ # local filesystem compliant path.
288
+ #
289
+ # Example:
290
+ # parser.on(...) {|dir| options[:output] = dir }
291
+ # ...
292
+ # config_path :output, 'subdir1'
293
+ # config_path :output, 'subdir2'
294
+
295
+ def config_path(*path)
296
+ File.join(path.map {|item|
297
+ case item
298
+ when Symbol
299
+ options.fetch(item)
300
+ else
301
+ item.to_s
302
+ end
303
+ })
304
+ end
305
+
306
+ # :call-seq:
307
+ # config_dir '', 'path', 'name'
308
+ # config_dir :option, 'subdir', ...
309
+ #
310
+ # Creates a directory if it doesn't already exist.
311
+ #
312
+ # Uses config_path to construct the path, so any symbols will
313
+ # be looked up in the options hash.
314
+ #
315
+ # Example:
316
+ #
317
+ # config_dir :output, system.name
318
+
319
+ def config_dir(*path)
320
+ Dir.mkdir(config_path(*path))
321
+ rescue Errno::EEXIST
322
+ nil
323
+ end
324
+
325
+ # :call-seq:
326
+ # config_file 'filename' {|file| ... }
327
+ # config_file :option, 'fragment', 'base.ext' {|file| ... }
328
+ # config_file ..., mode:0600 {|file| ...}
329
+ #
330
+ # Creates a configuration file, with default mode:0644
331
+ # and calls the given block with the file as argument.
332
+ #
333
+ # Uses config_path to construct the path, so any symbols will
334
+ # be looked up in the options hash.
335
+ #
336
+ # N.B! If the file already exists, it will be overwritten!
337
+ #
338
+ # Example:
339
+ #
340
+ # config_file :output, system.name, 'dump.yaml' do |file|
341
+ # file << system.to_yaml
342
+ # end
343
+
344
+ def config_file(*path, mode:0644, &block)
345
+ File.open(config_path(*path),
346
+ File::CREAT | File::TRUNC | File::WRONLY, mode) do |file|
347
+ block.call(file)
348
+ end
349
+ end
350
+
351
+ # Create a new pallet directory inside the warehouse
352
+ #
353
+ # Uses config_dir to create the directory, so any symbols will
354
+ # be looked up in the options hash.
355
+ #
356
+ # This is effectively a noop if the pallet already exists.
357
+ #
358
+ # Example:
359
+ #
360
+ # pallet_dir 'system', :system_name
361
+
362
+ def pallet_dir(kind, name)
363
+ config_dir :warehouse, kind
364
+ config_dir :warehouse, kind, name
365
+ end
366
+
367
+ # Write a key box file inside a pallet
368
+ #
369
+ # The block should return a hash representing the contents of the box.
370
+ # All keys will be stringified, so we can use key: short forms for
371
+ # declaration of the box contents.
372
+ #
373
+ # Uses config_file to create the file, so any symbols will
374
+ # be looked up in the options hash.
375
+ #
376
+ # N.B! If the box already exists, it will be overwritten!
377
+ #--
378
+ # FIXME: should new values be merged with existing box data instead?
379
+ #++
380
+ #
381
+ # Example:
382
+ #
383
+ # pallet_box 'domain', :domain, 'dns' do
384
+ # { dns:{ ns:options[:soa_ns].split(',') } }
385
+ # end
386
+
387
+ def pallet_box(kind, name, box, &block)
388
+ config_file :warehouse, kind, name, "#{box}.yaml" do |box_file|
389
+ box_file << block.call.deep_stringify_keys.to_yaml
390
+ end
391
+ end
392
+
393
+ # Create links from a pallet to parents
394
+ #
395
+ # Uses config_path to construct paths within the warehouse, so any
396
+ # symbols will be looked up in the options hash.
397
+ #
398
+ # +links+ is a hash containing +link_type+=>[+parent_kind+, +parent_name+]
399
+ #
400
+ # If the link target is empty (e.g. +link_type+=>[]), the link is removed.
401
+ #
402
+ # Example:
403
+ #
404
+ # pallet_links 'system', :system, 'os'=>['os', :os], 'netinstall'=>[]
405
+
406
+ def pallet_links(kind, name, links={})
407
+ links.each do |link_type, parent|
408
+ link_path = config_path(:warehouse, kind, name, link_type)
409
+
410
+ begin
411
+ File.delete(link_path)
412
+ rescue Errno::ENOENT
413
+ nil
414
+ end
415
+ unless parent.empty?
416
+ parent_kind, parent_name = parent
417
+ parent_path = config_path('..', '..', parent_kind, parent_name)
418
+
419
+ File.symlink(parent_path, link_path)
420
+ end
421
+ end
422
+ end
423
+
424
+ # Return a string stating the Git provenance of a warehouse directory,
425
+ # suitable for inclusion at the top of a generated configuration file,
426
+ # with each line prefixed by +comment_char+.
427
+ #
428
+ # If <tt>options[:warehouse]</tt> points within a Git repository,
429
+ # return a string stating its absolute path and active branch. If
430
+ # +include_id+ is true, also include the commit ID of the branch's
431
+ # HEAD.
432
+ #
433
+ # If Git information cannot be found for
434
+ # <tt>options[:warehouse]</tt>, return a string stating its path
435
+ # and print an error message on stderr.
436
+
437
+ def git_header(tool_name, comment_char: '#', include_id: false)
438
+ repo = Rugged::Repository.discover(options[:warehouse])
439
+ workdir = repo.workdir
440
+ branch = repo.head.name
441
+ commit = repo.head.target_id
442
+
443
+ header =
444
+ "#{comment_char}#{comment_char}
445
+ #{comment_char}#{comment_char} Automatically generated by #{tool_name} from
446
+ #{comment_char}#{comment_char} Repository: #{workdir}
447
+ #{comment_char}#{comment_char} Branch: #{branch}\n"
448
+ if include_id
449
+ then
450
+ header +=
451
+ "#{comment_char}#{comment_char} Commit ID: #{repo.head.target_id}\n"
452
+ end
453
+ header += "#{comment_char}#{comment_char}\n"
454
+ return header
455
+ rescue
456
+ STDERR.puts "Error finding Git sourcing information: #{$!}"
457
+ return "#{comment_char}#{comment_char}
458
+ #{comment_char}#{comment_char} Automatically generated by #{tool_name} from
459
+ #{comment_char}#{comment_char} Warehouse: #{warehouse_path}
460
+ #{comment_char}#{comment_char}\n"
461
+ end
462
+ end
463
+ end