foobar_templates 2.0.0 → 2.0.1.rc2

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: 8ce07f040ecc4ee93a6ee5b6d091af7fdecda0d95f36d17404f900cb48086fa9
4
- data.tar.gz: dd0c320b9a1176f5873889fc18b9304923a2771383f426bc053a9158ed98e76a
3
+ metadata.gz: ef1e2cd0e959fef6c0ac4f5e4475675f8ea8c9b4c60d818ff3606ca94d2638a7
4
+ data.tar.gz: e5d6e2ef6a066ad687a0509eb80d199f7d381307bf4ea2470f8f45f9c6eca274
5
5
  SHA512:
6
- metadata.gz: 9ed9a0c22a22e58eb73124062cd444bc547e1ce0d5ad48bf10942d68c571b3c3d15395aaeef732406ca77fe6e56ce561aa94ccefce39837ac170ebe513eb54eb
7
- data.tar.gz: 1c09a14c319a55532052e0ece16582b7876a34f73377de7821c4ed0a40927483ffa675f56f843422cf8a31670d9cf3c26a4e97b29a0d99e54cc034e8dac7912b
6
+ metadata.gz: 59087e7c7d265a35bc574ef126d11ff253b3c4970ce156e1d3edc0bbfb67d73f39f0894498f6594f9eb2e63e1fc7307803fe9e243bfe000fae7bc985d14b3484
7
+ data.tar.gz: cd0ee89ddd0b52b6ab18c222deb7af07a933cc253428861ffd6e276943b9f70225e95ca8a95e4a502a89989358596d372020d75628fe532bfab9c048fe70f336
@@ -0,0 +1,24 @@
1
+ name: Build
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build gem
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: '3.3'
16
+ bundler-cache: true
17
+ - name: Build gem
18
+ run: bundle exec rake build
19
+ - name: Upload gem artifact
20
+ uses: actions/upload-artifact@v4
21
+ with:
22
+ name: foobar_templates-gem
23
+ path: pkg/*.gem
24
+ if-no-files-found: error
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ test:
10
+ name: Test (Ruby ${{ matrix.ruby }})
11
+ runs-on: ubuntu-latest
12
+ continue-on-error: ${{ matrix.experimental || false }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ['3.0', '3.2', '3.3', '3.4', '4.0']
17
+ include:
18
+ - ruby: head
19
+ experimental: true
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+ - name: Run specs
27
+ run: bundle exec rake spec
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Foobar Templates: A Pain-Free Project Templator
2
- [![Gem Version](https://badge.fury.io/rb/foobar_templates.svg)](https://badge.fury.io/rb/foobar_templates)
2
+ [![Gem Version](https://badge.fury.io/rb/foobar_templates.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/foobar_templates)
3
3
 
4
4
  Foobar Templates allow users to define project templates in the most native form to all technologist: Directory Structures, short commands, and helpful commands that make the tool's usage completely visible!
5
5
 
@@ -14,6 +14,7 @@ I highly recommend the `foobar` alias!
14
14
  ```bash
15
15
  gem install foobar_templates
16
16
  foobar_templates --install-public-templates
17
+ foobar_templates --setup-personal-templates
17
18
 
18
19
  echo "alias foobar='foobar_templates'" >> ~/.bashrc
19
20
  source ~/.bashrc
data/Rakefile CHANGED
@@ -1,13 +1,119 @@
1
1
  require 'rake'
2
+ require 'rubygems'
2
3
  require 'rspec/core/rake_task'
3
4
 
4
5
  GEM_NAME = "foobar_templates"
5
6
  GEM_SPEC = "#{GEM_NAME}.gemspec"
7
+ VERSION_FILE = "lib/#{GEM_NAME}/version.rb"
8
+
9
+ module ReleaseHelper
10
+ module_function
11
+
12
+ # Prerelease syntax: `MAJOR.MINOR.PATCH.rcN` (e.g. `2.0.1.rc1`).
13
+ # Gem apparently doesn't support semver's `-` syntax =/
14
+ RC_RE = /\A(\d+)\.(\d+)\.(\d+)\.rc(\d+)\z/
15
+ REL_RE = /\A(\d+)\.(\d+)\.(\d+)\z/
16
+
17
+ def dry_run?
18
+ ENV['RELEASE_DRY_RUN'] == '1'
19
+ end
20
+
21
+ def current_version
22
+ src = File.read(VERSION_FILE)
23
+ m = src.match(/VERSION\s*=\s*"([^"]+)"/)
24
+ abort "Could not parse VERSION from #{VERSION_FILE}" unless m
25
+ m[1]
26
+ end
27
+
28
+ def next_version(v)
29
+ if (m = v.match(RC_RE))
30
+ maj, min, pat, rc = m.captures.map(&:to_i)
31
+ "#{maj}.#{min}.#{pat}.rc#{rc + 1}"
32
+ elsif (m = v.match(REL_RE))
33
+ maj, min, pat = m.captures.map(&:to_i)
34
+ "#{maj}.#{min}.#{pat + 1}.rc1"
35
+ else
36
+ abort "Unrecognized version format: #{v.inspect} (expected MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH.rcN)"
37
+ end
38
+ end
39
+
40
+ def write_version!(new_version)
41
+ src = File.read(VERSION_FILE)
42
+ updated = src.sub(/VERSION\s*=\s*"[^"]+"/, %(VERSION = "#{new_version}"))
43
+ File.write(VERSION_FILE, updated)
44
+ end
45
+
46
+ def confirm!(prompt, default: false)
47
+ suffix = default ? "[Y/n]" : "[y/N]"
48
+ print "#{prompt} #{suffix} "
49
+ answer = $stdin.gets&.strip
50
+ return default if answer.nil? || answer.empty?
51
+ %w[y yes].include?(answer.downcase)
52
+ end
53
+
54
+ def sh!(cmd)
55
+ puts "+ #{cmd}"
56
+ abort "command failed: #{cmd}" unless system(cmd)
57
+ end
58
+
59
+ def push!(cmd)
60
+ if dry_run?
61
+ puts "[DRY-RUN] would run: #{cmd}"
62
+ else
63
+ sh!(cmd)
64
+ end
65
+ end
66
+
67
+ def ensure_clean_git!
68
+ out = `git status --porcelain`
69
+ return if out.strip.empty?
70
+ abort "Working tree is dirty. Commit or stash changes before releasing.\n#{out}"
71
+ end
72
+
73
+ def ensure_on_default_branch!
74
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
75
+ return if %w[main master].include?(branch)
76
+ return if confirm!("You are on branch '#{branch}', not main/master. Continue?", default: false)
77
+ abort "Aborted by user."
78
+ end
79
+
80
+ def working_tree_changed?
81
+ !`git status --porcelain`.strip.empty?
82
+ end
83
+
84
+ # Make sure this machine has push access to the gem repository
85
+ def preflight!
86
+ problems = []
87
+
88
+ cred_paths = [
89
+ File.expand_path("~/.gem/credentials"),
90
+ File.expand_path("~/.local/share/gem/credentials"),
91
+ ]
92
+ cred_path = cred_paths.find { |p| File.exist?(p) }
93
+ if cred_path.nil?
94
+ problems << "No rubygems credentials found (looked in #{cred_paths.join(', ')}). Run `gem signin`."
95
+ elsif (File.stat(cred_path).mode & 0o077) != 0
96
+ problems << "Rubygems credentials at #{cred_path} are world/group-readable. Run: chmod 0600 #{cred_path}"
97
+ end
98
+
99
+ unless problems.empty?
100
+ abort "Release preflight failed:\n - #{problems.join("\n - ")}"
101
+ end
102
+
103
+ puts "Checking rubygems push access for #{GEM_NAME}..."
104
+ owners_out = `gem owner #{GEM_NAME} </dev/null 2>&1`
105
+ unless $?.success?
106
+ abort "Cannot query rubygems owners for #{GEM_NAME}:\n#{owners_out}\nRun `gem signin` and ensure your API key has push scope."
107
+ end
108
+ end
109
+ end
6
110
 
7
111
  desc "Build #{GEM_NAME} gem"
8
112
  task :build do
9
- system "gem build #{GEM_SPEC}"
10
113
  FileUtils.mkdir_p "pkg"
114
+ FileUtils.rm_f Dir.glob("pkg/#{GEM_NAME}-*.gem")
115
+ FileUtils.rm_f Dir.glob("#{GEM_NAME}-*.gem")
116
+ system "gem build #{GEM_SPEC}"
11
117
  FileUtils.mv Dir.glob("#{GEM_NAME}-*.gem"), "pkg/"
12
118
  end
13
119
 
@@ -16,10 +122,50 @@ task install: :build do
16
122
  system "gem install pkg/#{Dir.children('pkg').sort.last}"
17
123
  end
18
124
 
19
- desc "Build and push #{GEM_NAME} gem to RubyGems"
20
- task release: :build do
21
- gem_file = Dir.glob("pkg/#{GEM_NAME}-*.gem").sort.last
22
- system "gem push #{gem_file}"
125
+ desc "Interactive local release: confirm version, run specs, build, tag, push, and bump"
126
+ task :release do
127
+ include_helper = ReleaseHelper
128
+
129
+ include_helper.ensure_clean_git!
130
+ include_helper.ensure_on_default_branch!
131
+ include_helper.preflight!
132
+
133
+ version = include_helper.current_version
134
+ puts "Current version in #{VERSION_FILE}: #{version}"
135
+ unless include_helper.confirm!("Release version #{version}?", default: false)
136
+ puts "Aborted. Edit #{VERSION_FILE} to change the version, then re-run `rake release`."
137
+ exit 0
138
+ end
139
+
140
+ Rake::Task[:spec].invoke
141
+ Rake::Task[:build].invoke
142
+
143
+ gem_file = "pkg/#{GEM_NAME}-#{version}.gem"
144
+ abort "Built gem not found at #{gem_file}" unless File.exist?(gem_file)
145
+
146
+ if include_helper.working_tree_changed?
147
+ include_helper.sh! "git add #{VERSION_FILE}"
148
+ include_helper.sh! %(git commit -m "Release v#{version}")
149
+ end
150
+
151
+ tag = "v#{version}"
152
+ include_helper.sh! %(git tag -a #{tag} -m "Release #{tag}")
153
+ include_helper.push! "git push origin HEAD"
154
+ include_helper.push! "git push origin #{tag}"
155
+ include_helper.push! "gem push #{gem_file}"
156
+
157
+ next_v = include_helper.next_version(version)
158
+ include_helper.write_version!(next_v)
159
+ include_helper.sh! "git add #{VERSION_FILE}"
160
+ include_helper.sh! %(git commit -m "Bump to v#{next_v}")
161
+ include_helper.push! "git push origin HEAD"
162
+
163
+ puts ""
164
+ puts "=" * 60
165
+ puts "Released: #{tag}"
166
+ puts "Next development version: #{next_v}"
167
+ puts "Tag: https://github.com/TheNotary/#{GEM_NAME}/releases/tag/#{tag}"
168
+ puts "=" * 60
23
169
  end
24
170
 
25
171
  desc "Run unit specs"
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
@@ -271,39 +270,67 @@ module FoobarTemplates::CLI
271
270
 
272
271
  # Returns a hash of source directory names and their destination mappings
273
272
  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)
280
-
281
- [base_path, substitute_template_values(base_path)]
282
- end.to_h
283
- filter_ignored_files!(@template_src, template_dirs)
273
+ template_relative_paths.each_with_object({}) do |rel, dirs|
274
+ next unless File.directory?(File.join(@template_src, rel))
284
275
 
285
- template_dirs
276
+ dirs[rel] = substitute_template_values(rel)
277
+ end
286
278
  end
287
279
 
288
280
  # Figures out the translation between all template files and their
289
281
  # destination names
290
282
  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)
283
+ template_files = template_relative_paths.each_with_object({}) do |rel, files|
284
+ next if rel == "foobar.yml"
285
+ next unless File.file?(File.join(@template_src, rel))
297
286
 
298
- [base_path, substitute_template_values(base_path)]
299
- end.to_h
287
+ files[rel] = substitute_template_values(rel)
288
+ end
300
289
 
301
290
  raise_no_files_in_template_error! if template_files.empty?
302
- filter_ignored_files!(@template_src, template_files)
303
291
 
304
292
  return template_files
305
293
  end
306
294
 
295
+ # Enumerates every relative path under the template source, skipping the
296
+ # .git directory and any gitignored paths. Ignored directories are pruned
297
+ # during traversal so their (potentially huge) contents are never walked.
298
+ def template_relative_paths
299
+ @template_relative_paths ||= collect_non_ignored_paths(@template_src)
300
+ end
301
+
302
+ # Breadth-first walk that prunes ignored directories. One batched
303
+ # `git check-ignore` call is made per directory depth level, so we never
304
+ # descend into (or enumerate) an ignored subtree such as node_modules.
305
+ def collect_non_ignored_paths(root)
306
+ results = []
307
+ frontier = [nil] # relative dirs to scan at the current level; nil == root
308
+
309
+ until frontier.empty?
310
+ level_children = []
311
+ frontier.each do |rel_dir|
312
+ abs_dir = rel_dir ? File.join(root, rel_dir) : root
313
+ Dir.children(abs_dir).each do |name|
314
+ next if name == ".git"
315
+
316
+ level_children << (rel_dir ? File.join(rel_dir, name) : name)
317
+ end
318
+ end
319
+ break if level_children.empty?
320
+
321
+ ignored = ignored_paths(root, level_children)
322
+ next_frontier = []
323
+ level_children.each do |rel|
324
+ next if ignored.include?(rel)
325
+
326
+ results << rel
327
+ next_frontier << rel if File.directory?(File.join(root, rel))
328
+ end
329
+ frontier = next_frontier
330
+ end
331
+
332
+ results
333
+ end
307
334
 
308
335
  # Applies literal foo-bar variant substitutions to path strings
309
336
  def substitute_template_values(path_str)
@@ -351,12 +378,19 @@ module FoobarTemplates::CLI
351
378
  chunk.nil? || chunk.include?("\x00")
352
379
  end
353
380
 
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) }
381
+ # Returns the subset of the given relative paths that git considers ignored.
382
+ # Paths are streamed via NUL-delimited stdin rather than argv to avoid the
383
+ # OS ARG_MAX limit ("Arg list too long") and to handle paths containing
384
+ # spaces or newlines. Returns an empty set when root is not a git repo.
385
+ def ignored_paths(root, rel_paths)
386
+ return Set.new if rel_paths.empty?
387
+
388
+ stdin_data = rel_paths.join("\x00")
389
+ stdout, _, _status = Open3.capture3(
390
+ "git", "-C", root.to_s, "check-ignore", "-z", "--stdin",
391
+ stdin_data: stdin_data
392
+ )
393
+ stdout.split("\x00").to_set
360
394
  end
361
395
 
362
396
  def create_template_directories(template_directories, target)
@@ -1,3 +1,3 @@
1
1
  module FoobarTemplates
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.1.rc2"
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" }
@@ -506,7 +546,7 @@ describe FoobarTemplates do
506
546
 
507
547
  before :each do
508
548
  # Default: no remote — skip network calls.
509
- allow(FoobarTemplates).to receive(:remote_repo_exists?).and_return(false)
549
+ allow(FoobarTemplates::CLI::SetupPersonalTemplatesRepo).to receive(:remote_repo_exists?).and_return(false)
510
550
  end
511
551
 
512
552
  it "errors when repo_domain is not configured" do
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.0
4
+ version: 2.0.1.rc2
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
@@ -83,6 +83,8 @@ files:
83
83
  - ".beader/issues/6-add-monorepo-discovery-and-generation-test-coverage.yaml"
84
84
  - ".beader/issues/7-document-monorepo-template-configuration-and-selection.yaml"
85
85
  - ".beader/meta.yaml"
86
+ - ".github/workflows/build.yml"
87
+ - ".github/workflows/ci.yml"
86
88
  - ".gitignore"
87
89
  - ".rspec"
88
90
  - Gemfile
@@ -128,9 +130,9 @@ licenses:
128
130
  - MIT
129
131
  metadata:
130
132
  bug_tracker_uri: https://github.com/TheNotary/foobar_templates/issues
131
- changelog_uri: https://github.com/TheNotary/foobar_templates/releases/tag/v2.0.0
133
+ changelog_uri: https://github.com/TheNotary/foobar_templates/releases/tag/v2.0.1.rc2
132
134
  documentation_uri: https://github.com/TheNotary/foobar_templates
133
- source_code_uri: https://github.com/TheNotary/foobar_templates/tree/v2.0.0
135
+ source_code_uri: https://github.com/TheNotary/foobar_templates/tree/v2.0.1.rc2
134
136
  rdoc_options: []
135
137
  require_paths:
136
138
  - lib
@@ -145,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
147
  - !ruby/object:Gem::Version
146
148
  version: '0'
147
149
  requirements: []
148
- rubygems_version: 3.6.2
150
+ rubygems_version: 4.0.10
149
151
  specification_version: 4
150
152
  summary: This gem makes more gems!
151
153
  test_files: