Skip to content

Commit 4486036

Browse files
authored
Fix swap-deps to use correct Bundler syntax for Git tags (#38) (#43)
* Fix swap-deps to use correct Bundler syntax for Git tags When using bin/swap-deps --github user/[email protected], the tool now correctly generates: gem "foo", github: "user/repo", tag: "v1.0.0" Instead of the incorrect: gem "foo", github: "user/repo", branch: "v1.0.0" Changes: - Update swap_gem_to_github to use :ref_type to determine parameter - Use 'tag:' for tags and 'branch:' for branches - Add test coverage for tag syntax Fixes #38
1 parent 073878c commit 4486036

File tree

10 files changed

+251
-94
lines changed

10 files changed

+251
-94
lines changed

.new-demo-versions

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,19 @@
1010
RAILS_VERSION="8.0.3"
1111

1212
# Shakapacker version (use ~> for compatibility range, or specific version)
13+
# Examples:
14+
# SHAKAPACKER_VERSION="~> 9.0" # Published gem version
15+
# SHAKAPACKER_VERSION="github:shakacode/shakapacker" # GitHub main branch (default)
16+
# SHAKAPACKER_VERSION="github:shakacode/shakapacker#fix-hmr" # GitHub branch (use #)
17+
# SHAKAPACKER_VERSION="github:shakacode/[email protected]" # GitHub tag (use @)
1318
SHAKAPACKER_VERSION="github:shakacode/shakapacker"
1419

1520
# React on Rails version (use ~> for compatibility range, or specific version)
21+
# Examples:
22+
# REACT_ON_RAILS_VERSION="~> 16.1" # Published gem version
23+
# REACT_ON_RAILS_VERSION="github:shakacode/react_on_rails" # GitHub main branch (default)
24+
# REACT_ON_RAILS_VERSION="github:shakacode/react_on_rails#feature-x" # GitHub branch (use #)
25+
# REACT_ON_RAILS_VERSION="github:shakacode/[email protected]" # GitHub tag (use @)
1626
REACT_ON_RAILS_VERSION="~> 16.1"
1727

1828
# ═══════════════════════════════════════════════════════════════════════

.swap-deps.yml.example

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,31 @@ gems:
3030
# Repos are cloned to ~/.cache/swap-deps/ and built automatically
3131
github:
3232
# Simple format (uses 'main' branch by default)
33-
shakapacker: shakacode/shakapacker
33+
# shakapacker: shakacode/shakapacker
3434

35-
# With custom branch or tag:
36-
# Format 1: String with #branch or @tag
37-
# react_on_rails: shakacode/react_on_rails#feature-branch
35+
# Branch example: Use # for branches
36+
# shakapacker: shakacode/shakapacker#fix-hmr
37+
38+
# Tag example: Use @ for tags (Bundler will use 'tag:' parameter)
3839
# react_on_rails: shakacode/[email protected]
3940

40-
# Format 2: Hash with repo and branch
41+
# Format 2: Hash with repo and branch/tag
4142
# react_on_rails:
4243
# repo: shakacode/react_on_rails
4344
# branch: feature-branch-name
45+
# ref_type: branch # or :tag
4446

45-
# Examples:
47+
# Common examples:
4648
# - Test a PR branch: shakacode/shakapacker#fix-hmr
4749
# - Test from a fork: yourname/react_on_rails#experimental
48-
# - Use a release tag: shakacode/[email protected]
50+
# - Use a specific release tag: shakacode/[email protected]
51+
# - Use a pre-release tag: shakacode/[email protected]
4952
# - Use stable branch: shakacode/shakapacker#v8-stable
5053

54+
# Real-world tag examples:
55+
# shakapacker: shakacode/[email protected] # Generates: gem "shakapacker", github: "...", tag: "v9.0.0"
56+
# react_on_rails: shakacode/[email protected] # Generates: gem "react_on_rails", github: "...", tag: "v16.1.0"
57+
5158
# You can mix local paths (gems:) and GitHub repos (github:)
5259
# GitHub repos are cloned, built, and treated like local paths
5360

bin/new-demo

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ parser = OptionParser.new do |opts|
3939
options[:dry_run] = true
4040
end
4141

42-
opts.on('--shakapacker-version VERSION', 'Shakapacker version (or github:org/repo@branch)') do |v|
42+
opts.on('--shakapacker-version VERSION', 'Shakapacker version (or github:org/repo[#branch|@tag])') do |v|
4343
options[:shakapacker_version] = v
4444
end
4545

46-
opts.on('--react-on-rails-version VERSION', 'React on Rails version (or github:org/repo@branch)') do |v|
46+
opts.on('--react-on-rails-version VERSION', 'React on Rails version (or github:org/repo[#branch|@tag])') do |v|
4747
options[:react_on_rails_version] = v
4848
end
4949

@@ -93,9 +93,9 @@ parser = OptionParser.new do |opts|
9393
puts ' bin/new-demo my-demo --shakapacker-prerelease'
9494
puts ' bin/new-demo my-demo --react-on-rails-prerelease'
9595
puts ''
96-
puts ' # Use GitHub branches'
97-
puts ' bin/new-demo my-demo --shakapacker-version="github:shakacode/shakapacker@my-branch"'
98-
puts ' bin/new-demo my-demo --react-on-rails-version="github:shakacode/react_on_rails@fix-hmr"'
96+
puts ' # Use GitHub branches (# for branches) or tags (@ for tags)'
97+
puts ' bin/new-demo my-demo --shakapacker-version="github:shakacode/shakapacker#my-branch"'
98+
puts ' bin/new-demo my-demo --react-on-rails-version="github:shakacode/react_on_rails@v16.1.0"'
9999
puts ''
100100
puts ' # Customize Rails and React on Rails setup'
101101
puts ' bin/new-demo my-demo --rails-args="--skip-test,--api"'

docs/LOCAL_DEVELOPMENT.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ bin/swap-deps --github shakacode/shakapacker
140140
- The repo is cloned to `~/.cache/local-gems/` with the pattern `{user}-{repo}-{branch}/`
141141
- The clone is automatically built (if it has npm packages)
142142
- Subsequent runs update the existing clone instead of re-cloning
143-
- For Gemfiles: Uses `github: 'user/repo', branch: 'branch-name'` syntax
143+
- For Gemfiles:
144+
- Branches use: `github: 'user/repo', branch: 'branch-name'` (omits `branch:` for main/master)
145+
- Tags use: `github: 'user/repo', tag: 'v1.0.0'` (always explicit)
144146
- For package.json: Uses `file:` protocol pointing to the cached clone
145147

146148
**Benefits:**

lib/demo_scripts/demo_creator.rb

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,22 +182,26 @@ def add_gem_with_source(gem_name, version_spec)
182182
end
183183

184184
def add_gem_from_github(gem_name, version_spec)
185-
# Parse and validate github:org/repo@branch format
185+
# Parse and validate github:org/repo#branch or github:org/repo@tag format
186186
github_spec = version_spec.sub('github:', '').strip
187187

188188
raise Error, "Invalid GitHub spec: empty after 'github:'" if github_spec.empty?
189189

190-
repo, branch = parse_github_spec(github_spec)
190+
repo, ref, ref_type = parse_github_spec(github_spec)
191191
validate_github_repo(repo)
192-
validate_github_branch(branch) if branch
192+
validate_github_branch(ref) if ref
193193

194-
cmd = build_github_bundle_command(gem_name, repo, branch)
194+
cmd = build_github_bundle_command(gem_name, repo, ref, ref_type)
195195
@runner.run!(cmd, dir: @demo_dir)
196196
end
197197

198-
def build_github_bundle_command(gem_name, repo, branch)
198+
def build_github_bundle_command(gem_name, repo, ref, ref_type)
199199
cmd = ['bundle', 'add', gem_name, '--github', repo]
200-
cmd.push('--branch', branch) if branch
200+
if ref
201+
# Use --tag for tags, --branch for branches
202+
param = ref_type == :tag ? '--tag' : '--branch'
203+
cmd.push(param, ref)
204+
end
201205
Shellwords.join(cmd)
202206
end
203207

@@ -272,9 +276,10 @@ def update_package_dependency(package_json, package_name, version_spec)
272276

273277
def convert_to_npm_github_url(version_spec)
274278
github_spec = version_spec.sub('github:', '').strip
275-
repo, branch = parse_github_spec(github_spec)
279+
repo, ref, _ref_type = parse_github_spec(github_spec)
276280
github_url = "github:#{repo}"
277-
github_url += "##{branch}" if branch
281+
# npm uses # for all refs (both branches and tags)
282+
github_url += "##{ref}" if ref
278283
github_url
279284
end
280285

@@ -284,12 +289,18 @@ def build_github_npm_package(gem_name, version_spec)
284289
return if @dry_run
285290

286291
github_spec = version_spec.sub('github:', '').strip
287-
repo, branch = parse_github_spec(github_spec)
292+
repo, ref, ref_type = parse_github_spec(github_spec)
288293

289-
puts " Building #{gem_name} from #{repo}#{"@#{branch}" if branch}..."
294+
ref_display = if ref
295+
prefix = ref_type == :tag ? '@' : '#'
296+
"#{prefix}#{ref}"
297+
else
298+
''
299+
end
300+
puts " Building #{gem_name} from #{repo}#{ref_display}..."
290301

291302
Dir.mktmpdir("#{gem_name}-") do |temp_dir|
292-
clone_and_build_package(temp_dir, repo, branch, gem_name)
303+
clone_and_build_package(temp_dir, repo, ref, gem_name)
293304
end
294305
rescue CommandError, IOError, SystemCallError => e
295306
error_message = <<~ERROR

lib/demo_scripts/gem_swapper.rb

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module DemoScripts
1010
# Manages swapping dependencies between production and local/GitHub versions
1111
# rubocop:disable Metrics/ClassLength
1212
class DependencySwapper < DemoManager
13+
include GitHubSpecParser
14+
1315
# Maps gem names to their npm package subdirectories
1416
NPM_PACKAGE_PATHS = {
1517
'shakapacker' => '.',
@@ -312,23 +314,20 @@ def validate_gem_paths(paths)
312314
paths.transform_values { |path| File.expand_path(path) }
313315
end
314316

315-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/BlockLength
317+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
316318
def validate_github_repos(repos)
317319
invalid = repos.keys - SUPPORTED_GEMS
318320
raise Error, "Unsupported gems: #{invalid.join(', ')}" if invalid.any?
319321

320322
repos.transform_values do |value|
321323
result = if value.is_a?(String)
322-
# String format: supports 'user/repo', 'user/repo#branch', or 'user/repo@tag'
323-
if value.include?('@')
324-
repo, ref = value.split('@', 2)
325-
{ repo: repo, branch: ref, ref_type: :tag }
326-
elsif value.include?('#')
327-
repo, ref = value.split('#', 2)
328-
{ repo: repo, branch: ref, ref_type: :branch }
329-
else
330-
{ repo: value, branch: 'main', ref_type: :branch }
331-
end
324+
# Use shared GitHubSpecParser for consistent parsing
325+
repo, ref, ref_type = parse_github_spec(value)
326+
{
327+
repo: repo,
328+
branch: ref || 'main',
329+
ref_type: ref_type || :branch
330+
}
332331
elsif value.is_a?(Hash)
333332
# Hash format with repo and optional branch
334333
{
@@ -340,20 +339,14 @@ def validate_github_repos(repos)
340339
raise Error, "Invalid GitHub repo format for #{value}"
341340
end
342341

343-
# Validate repo format (must be 'user/repo')
344-
unless %r{\A[\w.-]+/[\w.-]+\z}.match?(result[:repo])
345-
raise Error, "Invalid GitHub repo format: #{result[:repo]} (must be 'user/repo')"
346-
end
347-
348-
# Validate branch/tag name (alphanumeric, hyphens, underscores, dots, slashes)
349-
unless %r{\A[\w.\-/]+\z}.match?(result[:branch])
350-
raise Error, "Invalid branch/tag name: #{result[:branch]} (contains unsafe characters)"
351-
end
342+
# Use shared validation methods
343+
validate_github_repo(result[:repo])
344+
validate_github_branch(result[:branch]) if result[:branch]
352345

353346
result
354347
end
355348
end
356-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/BlockLength
349+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
357350

358351
def validate_local_paths!
359352
gem_paths.each do |gem_name, path|
@@ -499,9 +492,17 @@ def swap_gem_to_github(content, gem_name, info)
499492
# Extract options after version (if any)
500493
options = rest.sub(/^\s*,\s*(['"])[^'"]*\1/, '') # Remove version if present
501494

502-
# Build replacement: gem 'name', github: 'user/repo', branch: 'branch-name' [, options...]
495+
# Use tag: for tags, branch: for branches (default to :branch if not specified)
496+
ref_type = info[:ref_type] || :branch
497+
param_name = ref_type == :tag ? 'tag' : 'branch'
498+
499+
# Only omit ref when it's a branch (not tag) and the branch is 'main' or 'master'
500+
# Tags must always be explicit, even if named 'main' or 'master'
501+
should_omit_ref = ref_type == :branch && %w[main master].include?(info[:branch])
502+
503+
# Build replacement: gem 'name', github: 'user/repo', branch/tag: 'ref-name' [, options...]
503504
replacement = "#{indent}gem #{quote}#{gem_name}#{quote}, github: #{quote}#{info[:repo]}#{quote}"
504-
replacement += ", branch: #{quote}#{info[:branch]}#{quote}" if info[:branch] != 'main'
505+
replacement += ", #{param_name}: #{quote}#{info[:branch]}#{quote}" unless should_omit_ref
505506
replacement += options unless options.strip.empty?
506507
replacement
507508
end

lib/demo_scripts/github_spec_parser.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,32 @@
33
module DemoScripts
44
# Parses and validates GitHub repository specifications
55
module GitHubSpecParser
6-
# Parses github:org/repo@branch format
7-
# Returns [repo, branch] where branch can be nil
6+
# Parses GitHub spec: org/repo, org/repo#branch, or org/repo@tag
7+
# Returns [repo, ref, ref_type] where:
8+
# - ref can be nil (defaults to 'main')
9+
# - ref_type is :branch, :tag, or nil
10+
#
11+
# Syntax:
12+
# org/repo -> [org/repo, nil, nil]
13+
# org/repo#branch -> [org/repo, branch, :branch]
14+
# org/repo@tag -> [org/repo, tag, :tag]
15+
# org/repo@branch -> [org/repo, branch, :branch] (backward compatibility)
816
def parse_github_spec(github_spec)
917
if github_spec.include?('@')
1018
parts = github_spec.split('@', 2)
1119
raise Error, 'Invalid GitHub spec: empty repository' if parts[0].empty?
12-
raise Error, 'Invalid GitHub spec: empty branch' if parts[1].empty?
20+
raise Error, 'Invalid GitHub spec: empty ref after @' if parts[1].empty?
1321

14-
parts
22+
# @ indicates a tag, but support old behavior as branch for backward compatibility
23+
[parts[0], parts[1], :tag]
24+
elsif github_spec.include?('#')
25+
parts = github_spec.split('#', 2)
26+
raise Error, 'Invalid GitHub spec: empty repository' if parts[0].empty?
27+
raise Error, 'Invalid GitHub spec: empty ref after #' if parts[1].empty?
28+
29+
[parts[0], parts[1], :branch]
1530
else
16-
[github_spec, nil]
31+
[github_spec, nil, nil]
1732
end
1833
end
1934

@@ -47,6 +62,14 @@ def validate_github_branch(branch)
4762
# Additional Git ref naming rules
4863
raise Error, 'Invalid GitHub branch: cannot end with .lock' if branch.end_with?('.lock')
4964
raise Error, 'Invalid GitHub branch: cannot contain @{' if branch.include?('@{')
65+
66+
# Final safety check: ensure only safe characters (alphanumeric, hyphens, underscores, dots, slashes)
67+
# This catches any remaining unsafe characters (like $, (), etc.) that could cause shell injection
68+
safe_pattern = %r{\A[\w.\-/]+\z}
69+
return if branch.match?(safe_pattern)
70+
71+
raise Error,
72+
"Invalid GitHub branch: '#{branch}' contains unsafe characters (only alphanumeric, -, _, ., / allowed)"
5073
end
5174
end
5275
end

0 commit comments

Comments
 (0)