foobar_templates 2.0.1.rc1 → 2.0.1.rc3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 247c1ea143d5bffcb645aa6ee4ce0793a8962869b100e268c424697d77b8b4c8
4
- data.tar.gz: c6fe7ccc5771db07b24c61d8d640f3516f2b73308d1937334df69069139658cd
3
+ metadata.gz: f6f830e123079c5b60a6cd7f799df40f0615b6326565814e837029ea6a8d69e9
4
+ data.tar.gz: 8d19d2bad9ee276d489657f9de11ef0ac8e9929a92d7fa08d95b2f9e609bc9d4
5
5
  SHA512:
6
- metadata.gz: 78e4608ba4ec94b0a0271f7753ae5b65d79cbbe6dec0a36d42cce040bdbd78353ba872158abce356cb82c0e98e1198eedf28becdc4956d36be0b3750821151db
7
- data.tar.gz: fb918a1c50461ed3bc66e0c3c54fc1f0ba50ae0f0bcd04d381810a7009563fbfaec5142e3c111329c2c1f69b44df31c30b17f447ea6de45c0afa26527cb05d76
6
+ metadata.gz: 2e2abe9daa3749abab39f10fbb110b8262541fd2ebba6f74d4c6d0bf85fe97d3e05a6745f05cb05fbb27db532c2c2d99945afe028e90dba066335648669ad998
7
+ data.tar.gz: df73ec40e11c12ac8316ab4c6105ac0d9ea895f2ca049b6b676b44660d7656df8041411ef38775d30e98f1d5a6909c2e4cb70e0210769b94d69eebfa6ffa7025
data/bin/foobar_templates CHANGED
@@ -29,6 +29,10 @@ parser = OptionParser.new do |opts|
29
29
  exit
30
30
  end
31
31
 
32
+ opts.on("-p", "--performance", "Show performance metrics for template generation") do
33
+ options[:performance] = true
34
+ end
35
+
32
36
  opts.on("--install-public-templates", "Install public templates") do
33
37
  FoobarTemplates.install_public_templates
34
38
  exit
data/changelog CHANGED
@@ -1,5 +1,6 @@
1
1
  ** Unreleased **
2
2
  - Rename: project renamed from `bundlegem` to `foobar_templates`. Module is now `FoobarTemplates`, executable is `foobar_templates`, per-template config file is `foobar.yml`, and user config/templates live under `~/.foobar/`. No backward-compatibility shims.
3
+ - Bugfix: fixes `Arg list too long` crash when generating from templates containing many gitignored files (e.g. node_modules); ignored directories are now pruned during a breadth-first traversal and path checks are streamed to `git check-ignore` via NUL-delimited stdin instead of argv.
3
4
 
4
5
  ** Planned 0.2.x **
5
6
  - Feature: templates can declare a `name_validation:` block in bundlegem.yml with two optional keys: `reserved_names` (exact-match denylist) and `regex_validator` (a Ruby regex the project name must match). Validation runs after the built-in regex check and before any files are written. All checks happen in pure Ruby — no shell, no cross-platform concerns.
data/config/config CHANGED
@@ -3,6 +3,6 @@
3
3
  default_template: cli_gem
4
4
  public_templates: https://github.com/TheNotary/template-builtins
5
5
  registry_domain: null
6
- repo_domain: null
7
- k8s_domain: null
6
+ repo_domain: null # github.com
7
+ k8s_domain: null # k8s.example.com
8
8
  always_perform_git_init: false
@@ -1,7 +1,6 @@
1
1
  require 'pathname'
2
2
  require 'yaml'
3
3
  require 'open3'
4
- require 'shellwords'
5
4
  require 'set'
6
5
 
7
6
  $TRACE = false
@@ -24,53 +23,67 @@ module FoobarTemplates::CLI
24
23
  end
25
24
 
26
25
  def config
27
- @config ||= build_interpolation_config
26
+ @config ||= time_it("build_interpolation_config") { build_interpolation_config }
28
27
  end
29
28
 
30
29
  def run
31
- puts "Beginning run" if $TRACE
32
- raise_project_with_that_name_already_exists! if File.exist?(target)
30
+ time_it("TOTAL run") do
31
+ puts "Beginning run" if $TRACE
32
+ raise_project_with_that_name_already_exists! if File.exist?(target)
33
33
 
34
- puts "ensure_safe_project_name" if $TRACE
35
- ensure_safe_project_name(name, config[:constant_array])
34
+ puts "ensure_safe_project_name" if $TRACE
35
+ time_it("ensure_safe_project_name") do
36
+ ensure_safe_project_name(name, config[:constant_array])
37
+ end
36
38
 
37
- puts "run_name_validation" if $TRACE
38
- run_name_validation
39
+ puts "run_name_validation" if $TRACE
40
+ time_it("run_name_validation") { run_name_validation }
39
41
 
40
- template_src = match_template_src
42
+ template_src = time_it("match_template_src") { match_template_src }
41
43
 
42
- puts "dynamically_generate_template_directories" if $TRACE
43
- time_it("dynamically_generate_template_directories") do
44
- @template_directories = dynamically_generate_template_directories
45
- end
44
+ puts "dynamically_generate_template_directories" if $TRACE
45
+ @template_directories = time_it("dynamically_generate_template_directories") do
46
+ dynamically_generate_template_directories
47
+ end
46
48
 
47
- puts "dynamically_generate_templates_files" if $TRACE
48
- templates = dynamically_generate_templates_files
49
+ puts "dynamically_generate_templates_files" if $TRACE
50
+ templates = time_it("dynamically_generate_templates_files") do
51
+ dynamically_generate_templates_files
52
+ end
49
53
 
50
- puts "Creating new project folder '#{name}'\n\n"
51
- create_template_directories(@template_directories, target)
54
+ puts "Creating new project folder '#{name}'\n\n"
55
+ time_it("create_template_directories") do
56
+ create_template_directories(@template_directories, target)
57
+ end
52
58
 
53
- templates.each do |src, dst|
54
- template("#{template_src}/#{src}", target.join(dst), config)
55
- end
59
+ time_it("write_template_files") do
60
+ templates.each do |src, dst|
61
+ template("#{template_src}/#{src}", target.join(dst), config)
62
+ end
63
+ end
56
64
 
57
- Dir.chdir(target) do
58
- if @configurator.always_perform_git_init || !inside_git_work_tree?
59
- `git init`
65
+ time_it("git_init_and_add") do
66
+ Dir.chdir(target) do
67
+ if @configurator.always_perform_git_init || !inside_git_work_tree?
68
+ `git init`
69
+ end
70
+ `git add .`
71
+ end
60
72
  end
61
- `git add .`
62
- end
63
73
 
64
- if @tconf[:bootstrap_command]
65
- puts "Executing bootstrap_command"
66
- cmd = safe_gsub_template_variables(@tconf[:bootstrap_command])
67
- puts cmd
68
- Dir.chdir(target) do
69
- `#{cmd}`
74
+ if @tconf[:bootstrap_command]
75
+ puts "Executing bootstrap_command"
76
+ cmd = safe_gsub_template_variables(@tconf[:bootstrap_command])
77
+ puts cmd
78
+ time_it("bootstrap_command") do
79
+ Dir.chdir(target) do
80
+ `#{cmd}`
81
+ end
82
+ end
70
83
  end
71
- end
72
84
 
73
- puts "\nComplete."
85
+ puts "\nComplete."
86
+ end
74
87
  end
75
88
 
76
89
  def build_interpolation_config
@@ -85,7 +98,9 @@ module FoobarTemplates::CLI
85
98
  git_user_email = `git config user.email`.chomp
86
99
 
87
100
  # Resolve domain values from ~/.foobar/config, prompting if needed
88
- required_domains = scan_template_for_required_domains
101
+ required_domains = time_it("scan_template_for_required_domains") do
102
+ scan_template_for_required_domains
103
+ end
89
104
  prompt_for_missing_domains(required_domains)
90
105
 
91
106
  registry_domain = @configurator.domain('registry_domain')
@@ -271,39 +286,69 @@ module FoobarTemplates::CLI
271
286
 
272
287
  # Returns a hash of source directory names and their destination mappings
273
288
  def dynamically_generate_template_directories
274
- template_dirs = Dir.glob("#{@template_src}/**/*", File::FNM_DOTMATCH).filter_map do |f|
275
- base_path = f[@template_src.length+1..-1]
276
- next if base_path.start_with?(".git" + File::SEPARATOR) || base_path == ".git"
277
- next if f == "#{@template_src}/." || f == "#{@template_src}/.."
278
- next unless File.directory?(f)
279
- # next if ignored_by_git?(@template_src, base_path)
289
+ template_relative_paths.each_with_object({}) do |rel, dirs|
290
+ next unless File.directory?(File.join(@template_src, rel))
280
291
 
281
- [base_path, substitute_template_values(base_path)]
282
- end.to_h
283
- filter_ignored_files!(@template_src, template_dirs)
284
-
285
- template_dirs
292
+ dirs[rel] = substitute_template_values(rel)
293
+ end
286
294
  end
287
295
 
288
296
  # Figures out the translation between all template files and their
289
297
  # destination names
290
298
  def dynamically_generate_templates_files
291
- template_files = Dir.glob("#{@template_src}/**/*", File::FNM_DOTMATCH).filter_map do |f|
292
- base_path = f[@template_src.length+1..-1]
293
- next if base_path.nil?
294
- next if base_path.start_with?(".git" + File::SEPARATOR) || base_path == ".git"
295
- next if base_path == "foobar.yml"
296
- next unless File.file?(f)
299
+ template_files = template_relative_paths.each_with_object({}) do |rel, files|
300
+ next if rel == "foobar.yml"
301
+ next unless File.file?(File.join(@template_src, rel))
297
302
 
298
- [base_path, substitute_template_values(base_path)]
299
- end.to_h
303
+ files[rel] = substitute_template_values(rel)
304
+ end
300
305
 
301
306
  raise_no_files_in_template_error! if template_files.empty?
302
- filter_ignored_files!(@template_src, template_files)
303
307
 
304
308
  return template_files
305
309
  end
306
310
 
311
+ # Enumerates every relative path under the template source, skipping the
312
+ # .git directory and any gitignored paths. Ignored directories are pruned
313
+ # during traversal so their (potentially huge) contents are never walked.
314
+ def template_relative_paths
315
+ @template_relative_paths ||= time_it("collect_non_ignored_paths") do
316
+ collect_non_ignored_paths(@template_src)
317
+ end
318
+ end
319
+
320
+ # Breadth-first walk that prunes ignored directories. One batched
321
+ # `git check-ignore` call is made per directory depth level, so we never
322
+ # descend into (or enumerate) an ignored subtree such as node_modules.
323
+ def collect_non_ignored_paths(root)
324
+ results = []
325
+ frontier = [nil] # relative dirs to scan at the current level; nil == root
326
+
327
+ until frontier.empty?
328
+ level_children = []
329
+ frontier.each do |rel_dir|
330
+ abs_dir = rel_dir ? File.join(root, rel_dir) : root
331
+ Dir.children(abs_dir).each do |name|
332
+ next if name == ".git"
333
+
334
+ level_children << (rel_dir ? File.join(rel_dir, name) : name)
335
+ end
336
+ end
337
+ break if level_children.empty?
338
+
339
+ ignored = ignored_paths(root, level_children)
340
+ next_frontier = []
341
+ level_children.each do |rel|
342
+ next if ignored.include?(rel)
343
+
344
+ results << rel
345
+ next_frontier << rel if File.directory?(File.join(root, rel))
346
+ end
347
+ frontier = next_frontier
348
+ end
349
+
350
+ results
351
+ end
307
352
 
308
353
  # Applies literal foo-bar variant substitutions to path strings
309
354
  def substitute_template_values(path_str)
@@ -351,12 +396,19 @@ module FoobarTemplates::CLI
351
396
  chunk.nil? || chunk.include?("\x00")
352
397
  end
353
398
 
354
- def filter_ignored_files!(repo_root, path_hash)
355
- cmd = "git -C #{repo_root} check-ignore #{Shellwords.join(path_hash.keys)}"
356
- stdout, _, status = Open3.capture3(cmd)
357
- filter_these_paths = stdout.split
358
-
359
- path_hash.delete_if { |key, _| filter_these_paths.include?(key) }
399
+ # Returns the subset of the given relative paths that git considers ignored.
400
+ # Paths are streamed via NUL-delimited stdin rather than argv to avoid the
401
+ # OS ARG_MAX limit ("Arg list too long") and to handle paths containing
402
+ # spaces or newlines. Returns an empty set when root is not a git repo.
403
+ def ignored_paths(root, rel_paths)
404
+ return Set.new if rel_paths.empty?
405
+
406
+ stdin_data = rel_paths.join("\x00")
407
+ stdout, _, _status = Open3.capture3(
408
+ "git", "-C", root.to_s, "check-ignore", "-z", "--stdin",
409
+ stdin_data: stdin_data
410
+ )
411
+ stdout.split("\x00").to_set
360
412
  end
361
413
 
362
414
  def create_template_directories(template_directories, target)
@@ -442,11 +494,18 @@ Exiting...
442
494
  end
443
495
 
444
496
  def time_it(label = nil)
497
+ return yield unless performance?
498
+
445
499
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
446
- yield
500
+ result = yield
447
501
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
448
502
  elapsed_ms = ((end_time - start_time) * 1000).round(2)
449
503
  puts "#{label || 'Elapsed'}: #{elapsed_ms} ms"
504
+ result
505
+ end
506
+
507
+ def performance?
508
+ @options[:performance]
450
509
  end
451
510
 
452
511
  # This checks to see that the gem_name is a valid ruby gem name and will 'work'
@@ -1,3 +1,3 @@
1
1
  module FoobarTemplates
2
- VERSION = "2.0.1.rc1"
2
+ VERSION = "2.0.1.rc3"
3
3
  end
@@ -128,6 +128,26 @@ describe FoobarTemplates do
128
128
  expect(src_dst_map['simple_dir']).to eq 'simple_dir'
129
129
  end
130
130
 
131
+ it "collect_non_ignored_paths walks files but prunes gitignored directories" do
132
+ template_dir = create_user_defined_template("testing", "template-user-supplied")
133
+ options = { bin: false, ext: false, coc: false, template: "template-user-supplied" }
134
+ my_gem = FoobarTemplates::CLI::TemplateGenerator.new(options, "good-dog")
135
+
136
+ File.write("#{template_dir}/.gitignore", "node_modules/\n")
137
+ File.write("#{template_dir}/README.md", "Hello")
138
+ FileUtils.mkdir_p("#{template_dir}/src")
139
+ File.write("#{template_dir}/src/main.rb", "puts 1")
140
+ FileUtils.mkdir_p("#{template_dir}/node_modules/big_pkg")
141
+ File.write("#{template_dir}/node_modules/big_pkg/index.js", "ignored")
142
+ `git init #{template_dir}`
143
+
144
+ paths = my_gem.send('collect_non_ignored_paths', template_dir)
145
+
146
+ expect(paths).to include("README.md", "src", "src/main.rb")
147
+ expect(paths).not_to include("node_modules")
148
+ expect(paths.grep(/node_modules/)).to be_empty
149
+ end
150
+
131
151
  it "returns the expected interpolated string when substitute_template_values is called" do
132
152
  options = { bin: false, ext: false, coc: false, template: "test_template" }
133
153
  gem_name = "good-dog"
@@ -173,6 +193,26 @@ describe FoobarTemplates do
173
193
  expect(File).not_to exist "#{@dst_dir}/#{gem_name}/node_modules"
174
194
  end
175
195
 
196
+ it "handles templates with a large number of gitignored files without overflowing the arg list" do
197
+ template_dir = create_user_defined_template("testing", "template-user-supplied")
198
+ options = { bin: false, ext: false, coc: false, template: "template-user-supplied" }
199
+ gem_name = "good-dog"
200
+
201
+ File.write("#{template_dir}/.gitignore", "node_modules/")
202
+ File.write("#{template_dir}/README.md", "Hello")
203
+ FileUtils.mkdir("#{template_dir}/node_modules")
204
+ # Enough files to blow past ARG_MAX if paths were passed as argv.
205
+ 5000.times { |i| File.write("#{template_dir}/node_modules/file_#{i}.rb", "ignored #{i}") }
206
+ `git init #{template_dir}`
207
+
208
+ capture_stdout do
209
+ FoobarTemplates.generate_template(options, gem_name)
210
+ end
211
+
212
+ expect(File).to exist "#{@dst_dir}/#{gem_name}/README.md"
213
+ expect(File).not_to exist "#{@dst_dir}/#{gem_name}/node_modules"
214
+ end
215
+
176
216
  it "executes the bootstrap_command if supplied" do
177
217
  template_dir = create_user_defined_template("testing", "template-user-supplied")
178
218
  options = { bin: false, ext: false, coc: false, template: "template-user-supplied" }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobar_templates
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1.rc1
4
+ version: 2.0.1.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - TheNotary
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -130,9 +130,9 @@ licenses:
130
130
  - MIT
131
131
  metadata:
132
132
  bug_tracker_uri: https://github.com/TheNotary/foobar_templates/issues
133
- changelog_uri: https://github.com/TheNotary/foobar_templates/releases/tag/v2.0.1.rc1
133
+ changelog_uri: https://github.com/TheNotary/foobar_templates/releases/tag/v2.0.1.rc3
134
134
  documentation_uri: https://github.com/TheNotary/foobar_templates
135
- source_code_uri: https://github.com/TheNotary/foobar_templates/tree/v2.0.1.rc1
135
+ source_code_uri: https://github.com/TheNotary/foobar_templates/tree/v2.0.1.rc3
136
136
  rdoc_options: []
137
137
  require_paths:
138
138
  - lib
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubygems_version: 3.6.2
150
+ rubygems_version: 4.0.10
151
151
  specification_version: 4
152
152
  summary: This gem makes more gems!
153
153
  test_files: