Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Oct 17, 2025

Summary

  • Fixes issue where swapping gems with GitHub refs fails with "Could not find gem" errors
  • Removes Gemfile.lock before running bundle install when swapping gem sources
  • Ensures clean resolution when switching between rubygems, GitHub, and local path sources

Problem

When swapping gems between different sources (rubygems ↔ GitHub ↔ local path), the Gemfile.lock can contain stale references:

  • GitHub refs may point to commits that no longer exist (after rebases/force pushes)
  • Switching from GitHub to rubygems leaves GitHub commit references in lock file
  • Bundle update fails with "Could not find gem 'name'" errors

Solution

Added clean_gemfile_lock method that removes the Gemfile.lock before running bundle commands when swapping gem sources. This forces Bundler to regenerate lock entries from scratch based on the updated Gemfile.

Test plan

  • Tested swapping to GitHub ref: bin/swap-deps --github 'shakacode/shakapacker#justin808/early-hints'
  • Verified bundle install works after swap
  • Tested restore functionality
  • Verified bundle install works after restore
  • RSpec tests pass (77 out of 86 - 6 pre-existing failures unrelated to this change)
  • RuboCop passes with no violations

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Detects and reports outdated GitHub-linked swapped gems and adds an --auto-update option (on by default) to optionally auto-update and rebuild affected packages.
    • Auto-detects project root and demo context; skips dependency setup for read-only commands.
  • Improvements

    • Switches to an install-after-cleanup flow, with clearer next-step guidance and more verbose lockfile/installation logging.
    • Provides verbose warnings when commit checks fail and optional npm rebuild triggering.
  • Bug Fixes

    • Removes stale lockfile entries before install to avoid resolution errors; updated install-failure messaging.
  • Tests

    • Updated tests to reflect the install-after-cleanup flow and revised messages.

When swapping gems between sources (rubygems ↔ GitHub ↔ local path),
the Gemfile.lock can contain stale references to commits or versions
that no longer exist, causing `bundle update` to fail with "Could not
find gem" errors.

This fix removes the Gemfile.lock before running bundle install when
swapping gem sources, forcing Bundler to generate fresh lock entries
from the updated Gemfile. This ensures:
- GitHub refs work correctly even if branches are rebased/force-pushed
- Switching from GitHub back to rubygems resolves cleanly
- Local path swaps don't conflict with previous GitHub/rubygems locks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

Walkthrough

Detects GitHub-based swapped gems that are behind their remotes, reports counts, optionally auto-updates caches, forces clean installs by backing up and removing Gemfile.lock when updating/restoring swapped gems, replaces many bundle update invocations with bundle install, and exposes an --[no-]auto-update CLI flag and read-only command detection.

Changes

Cohort / File(s) Summary
Outdated swapped-gems detection
lib/demo_scripts/gem_swapper.rb
Adds check_outdated_repos(demo_path) and check_commits_behind(cache_path, branch); integrates detection into show_status(auto_update:), collecting gem_name, repo, ref, and commits_behind.
Update / rebuild pathway
lib/demo_scripts/gem_swapper.rb
Adds update_outdated_repos(outdated_repos) to refresh GitHub caches and optionally rebuild associated npm packages.
Gemfile.lock cleanup & install flow
lib/demo_scripts/gem_swapper.rb
Adds clean_gemfile_lock(demo_path, gems_to_clean), restore_lock_backup(backup_path), cleanup_lock_backup(backup_path); modifies run_bundle_install to remove lockfile before installing and restore on failure; replaces several bundle update calls with bundle install and adds verbose logging and updated next-step messaging.
CLI flags, context & bundler loading
lib/demo_scripts/swap_deps_cli.rb
Adds attr_reader :auto_update (default true) and --[no-]auto-update flag; introduces read_only_command?, conditional bundler/setup require, project-root discovery (find_project_root) and demo detection (detect_demo_from_path), and switches working dir to project root when found.
Tests updated for new behavior
spec/demo_scripts/gem_swapper_spec.rb
Adjusts tests to expect clean_gemfile_lock prior to install, replaces bundle update expectations with bundle install --quiet, and updates error/message assertions and test descriptions to reflect install-after-clean semantics.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant CLI as swap_deps_cli
    participant Swapper as DependencySwapper
    participant Cache as LocalCache
    participant GitHub as RemoteRepo

    CLI->>Swapper: show_status(demo_path, auto_update?)
    Swapper->>Swapper: enumerate swapped gems (GitHub-based)
    loop per GitHub-swapped gem
        Swapper->>Cache: open cached repo (cache_path)
        Swapper->>GitHub: fetch remote ref (check_commits_behind)
        GitHub-->>Swapper: remote ref metadata
        Swapper-->>Swapper: compute commits_behind
    end
    alt outdated found & auto_update true
        Swapper->>Swapper: update_outdated_repos(outdated_repos)
    else outdated found & auto_update false
        Swapper-->>CLI: print manual update guidance
    end
    Swapper-->>CLI: print status summary
Loading
sequenceDiagram
    autonumber
    participant CLI as Caller
    participant Swapper as DependencySwapper
    participant FS as Filesystem
    participant Bundler as Bundler

    CLI->>Swapper: run_bundle_install(demo_path, gems_to_update, for_restore)
    alt gems_to_update present
        Swapper->>Swapper: compute gems_to_clean
        Swapper->>FS: backup & remove `Gemfile.lock` (clean_gemfile_lock)
    end
    Swapper->>Bundler: run `bundle install --quiet`
    Bundler-->>Swapper: success / failure
    alt failure
        Swapper->>FS: restore `Gemfile.lock` from backup (restore_lock_backup)
    else success
        Swapper->>FS: remove backup (cleanup_lock_backup)
    end
    Swapper-->>CLI: report result and print_next_steps
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇
I hop through refs and count what's late,
nudge a lock aside to let installs create.
I fetch, I patch, then tidy the track —
fresh gems in place, I scamper back. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Fix GitHub ref swapping by cleaning stale Gemfile.lock entries" accurately summarizes the primary objective and solution described in the PR. The title directly corresponds to the main changes: the addition of the clean_gemfile_lock method and enhanced run_bundle_install to remove stale lock file entries when swapping gem sources. The title is concise, specific, and free of vague language or noise. A teammate reviewing the git history would clearly understand that this PR addresses failures caused by stale Gemfile.lock entries during GitHub ref swapping operations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-github-refs

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
lib/demo_scripts/gem_swapper.rb (1)

1012-1027: Consider adding error handling for file deletion.

The approach of removing the lock file to force clean resolution is sound and addresses the PR objective. However, File.delete can raise exceptions (e.g., Errno::EACCES for permission issues) that aren't currently handled.

Consider wrapping the deletion in error handling:

     puts "  Cleaning lock file entries for: #{gems_to_clean.join(', ')}" if verbose
 
-    # Simply remove the lock file and let bundle regenerate it
-    # This is the safest approach to avoid any inconsistencies
-    File.delete(lock_file)
-    puts '  Removed Gemfile.lock to force clean resolution' if verbose
+    # Simply remove the lock file and let bundle regenerate it
+    # This is the safest approach to avoid any inconsistencies
+    begin
+      File.delete(lock_file)
+      puts '  Removed Gemfile.lock to force clean resolution' if verbose
+    rescue Errno::EACCES => e
+      warn "  ⚠️  Warning: Could not delete Gemfile.lock: #{e.message}"
+    rescue StandardError => e
+      warn "  ⚠️  Warning: Unexpected error removing Gemfile.lock: #{e.message}"
+    end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc9c22 and 827cf36.

📒 Files selected for processing (2)
  • lib/demo_scripts/gem_swapper.rb (2 hunks)
  • spec/demo_scripts/gem_swapper_spec.rb (3 hunks)
🔇 Additional comments (4)
spec/demo_scripts/gem_swapper_spec.rb (3)

734-749: LGTM! Test correctly verifies lock file cleanup before installation.

The test properly expects clean_gemfile_lock to be called with the correct arguments before bundle install, and the updated assertion aligns with the new installation-based approach instead of selective updates.


761-773: LGTM! Failure path test properly updated.

The test correctly verifies that clean_gemfile_lock is called even in the failure scenario and that the error message reflects the installation failure appropriately.


777-790: LGTM! Non-restore path test correctly updated.

The test properly verifies the lock file cleanup and installation flow for the swapping scenario (for_restore: false).

lib/demo_scripts/gem_swapper.rb (1)

1048-1070: LGTM! Integration correctly addresses stale lock file issues.

The integration of clean_gemfile_lock before bundle install in both restore and non-restore paths effectively prevents "Could not find gem" errors when swapping gem sources. The approach of removing the lock file and running a fresh install is conservative and safe.

The verbose logging helps with debugging, and the updated messaging accurately reflects the installation-based approach.

@claude
Copy link

claude bot commented Oct 17, 2025

Code Review - PR #59: Fix GitHub ref swapping by cleaning stale Gemfile.lock entries

Summary

This PR addresses a real problem with gem source swapping by removing stale Gemfile.lock entries before bundle operations. The solution is pragmatic and well-tested. Overall, this is a solid improvement with a few recommendations for enhancement.

Strengths

  1. Clear Problem Definition: The PR clearly identifies and solves the Could not find gem errors when swapping between gem sources
  2. Well-Documented: Excellent inline comments explaining why the lock file cleanup is necessary
  3. Test Coverage: Tests have been updated to reflect the new behavior and verify the clean_gemfile_lock method is called appropriately
  4. Consistent Approach: Uses the same pattern (remove lock file → bundle install) for both restore and swap operations

Code Quality Observations

Positive:

  • Method naming is clear and self-documenting (clean_gemfile_lock)
  • Guard clauses prevent unnecessary operations (lines 1016, 1019)
  • Verbose mode provides helpful user feedback (lines 1021, 1026)
  • Updated error messages accurately reflect the new behavior

Minor Suggestion - Add error handling for file deletion (lib/demo_scripts/gem_swapper.rb:1025):
File system operations can fail due to permissions, locked files, or other I/O issues. Consider wrapping File.delete in begin/rescue to handle Errno::EACCES and Errno::EPERM exceptions.

Potential Issues

  1. Missing standalone test coverage for clean_gemfile_lock - The new method is only tested indirectly through expectations in the run_bundle_install tests. Consider adding dedicated unit tests that verify: (a) deletes Gemfile.lock when gems_to_clean is present and file exists, (b) does nothing when gems_to_clean is empty, (c) does nothing when Gemfile.lock does not exist

  2. No verification that bundle install succeeds after lock file removal - The code assumes bundle install will work after removing the lock file, but failures happen with just a warning.

Performance Considerations

Nuclear approach trade-off: Deleting the entire Gemfile.lock forces Bundler to resolve ALL dependencies from scratch, not just the swapped gems. This is safer (guaranteed consistency) but slower (full dependency resolution vs. targeted update). For projects with many dependencies, this could add noticeable overhead. However, this trade-off is acceptable because: (1) Correctness > speed for this operation, (2) The overhead is one-time per swap operation, (3) Alternative approaches (surgical lock file editing) are complex and error-prone.

Security Considerations

No major security concerns identified. The code uses File.join to construct paths (prevents path traversal), checks file existence before deletion, runs in the context of the user's permissions, and uses Regexp.escape when constructing regex patterns (line 1008).

Minor hardening opportunity: Consider validating that demo_path is within expected directories to prevent accidental deletion of unrelated Gemfile.lock files.

Test Coverage Assessment

Current Coverage: Good - Tests verify clean_gemfile_lock is called with correct parameters, verify behavior changes (bundle update → bundle install), verify error messaging updates, and edge cases are covered (empty gems list, file doesn't exist).

Missing Coverage: Direct unit tests for clean_gemfile_lock method, error handling scenarios (permission denied, I/O errors), and dry-run mode for clean_gemfile_lock.

Suggestion: Add dry_run support to clean_gemfile_lock to maintain consistency with other methods in the codebase.

Recommendations Summary

High Priority:

  1. Add error handling for File.delete operation
  2. Add dedicated unit tests for clean_gemfile_lock method

Medium Priority:
3. Add dry_run mode support to clean_gemfile_lock
4. Consider adding a warning when lock file deletion falls back to the nuclear option

Low Priority:
5. Add path validation to ensure demo_path is in expected location

Conclusion

This is a well-thought-out solution to a legitimate problem. The approach of removing the entire lock file is pragmatic and safe, even if it's not the most performant option. The test updates demonstrate understanding of the behavior changes.

Recommendation: Approve with minor improvements - The suggestions above would make this excellent code even better, but they're not blockers for merging.

Great work!

justin808 and others added 2 commits October 16, 2025 18:23
Makes it clearer that users can enable auto-rebuild by adding the --watch
flag, instead of having to manually rebuild packages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Instead of showing a vague placeholder, now tells users to run
'bin/swap-deps --status' to see actual gem paths. This is much clearer
and actionable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 17, 2025

PR Review

I've reviewed PR #59 and here's my comprehensive feedback:

✅ Strengths

  1. Problem Well-Identified: The PR correctly identifies and solves a real issue with stale Gemfile.lock references when swapping gem sources
  2. Simple, Effective Solution: Removing the lock file and regenerating it is a pragmatic approach that avoids complex lock file parsing
  3. Good Documentation: Well-written comments explain the rationale for the changes
  4. Test Coverage: Tests have been updated to match the new behavior
  5. Consistent Approach: The fix is applied consistently in both restore and swap scenarios

🔍 Code Quality Observations

Positive:

  • Clean separation of concerns with the new clean_gemfile_lock method (lib/demo_scripts/gem_swapper.rb:1015)
  • Appropriate guard clauses prevent unnecessary operations
  • Verbose logging helps with debugging
  • Tests properly verify the new behavior with mocks

Minor Suggestions:

  1. Error Handling Enhancement (lib/demo_scripts/gem_swapper.rb:1025) - Consider wrapping File.delete in error handling to catch permission errors

  2. Return Value Consistency: The clean_gemfile_lock method doesn't return a success indicator. Consider returning true/false or raising an exception on failure.

🐛 Potential Issues

Low Priority:

  • Race Condition (Edge Case): If two processes run simultaneously, one might delete the lock file while the other is reading it. This is likely not a concern in typical usage but worth noting.

⚡ Performance Considerations

  • Positive Impact: Switching from bundle update to bundle install after lock file deletion is actually more efficient
  • Minor Trade-off: Deleting the entire lock file means all gems will be re-resolved, which takes slightly longer but ensures consistency

🔒 Security Concerns

  • No Issues Found: The changes don't introduce security vulnerabilities
  • File deletion is performed safely using standard library methods
  • No user input is directly used in file operations

🧪 Test Coverage

Good Coverage:

  • Tests verify lock file cleaning is called with correct parameters (spec/demo_scripts/gem_swapper_spec.rb:742, 765, 785)
  • Both success and failure scenarios are tested
  • Edge case of no supported gems is covered

Potential Test Gaps:

  1. No integration test verifying that File.delete is actually called
  2. No test for what happens if File.delete raises an exception
  3. Missing test for error handling scenarios

📝 Documentation

UI Improvement Noted:
The addition of --watch flag suggestion in the next steps output (lib/demo_scripts/gem_swapper.rb:1198-1199) is a nice UX enhancement.

🎯 Overall Assessment

Rating: ✅ Approve with Minor Suggestions

This is a solid PR that solves a real problem with a pragmatic approach. The core logic is sound, tests are adequate, and the code is maintainable. The minor suggestions above are optional improvements that would enhance robustness but aren't blockers for merging.

Recommendation: Approve and merge. Consider the error handling enhancement as a future improvement if issues arise in production.


Great work solving this bundler edge case! The approach of regenerating the lock file from scratch is much more reliable than trying to surgically edit it.

@claude
Copy link

claude bot commented Oct 17, 2025

Pull Request Review

Thank you for this fix! The approach of cleaning the Gemfile.lock to resolve stale GitHub ref issues is sound and well-implemented. Here is my detailed review:

Strengths

  1. Clear Problem and Solution: The PR addresses a real pain point when swapping gem sources, with excellent documentation explaining the issue.

  2. Good Code Quality:

    • Clear method naming (clean_gemfile_lock)
    • Appropriate comments explaining the why behind the approach
    • Follows Ruby style conventions
    • RuboCop passing with no violations
  3. Comprehensive Test Coverage:

    • Tests updated to verify clean_gemfile_lock is called in the right contexts
    • Both success and failure scenarios covered
    • Test descriptions updated to reflect new behavior
  4. User Experience:

    • Helpful verbose output explaining what is happening
    • Improved error messages (lib/demo_scripts/gem_swapper.rb:1055, 1068)
    • Better next-steps documentation (lib/demo_scripts/gem_swapper.rb:1197-1199)

Suggestions for Improvement

1. Consider Adding Error Handling for File Deletion

In clean_gemfile_lock (lib/demo_scripts/gem_swapper.rb:1025), File.delete could raise an exception if permissions are insufficient or the file is locked. Consider wrapping it in a begin/rescue block to handle Errno::EACCES or Errno::EPERM exceptions gracefully.

2. Potential Race Condition in Multi-Process Environments

If multiple processes run bundle operations simultaneously (unlikely but possible), deleting Gemfile.lock without atomic operations could cause issues. Consider adding a comment documenting the single-process execution assumption.

3. Test Coverage: Missing Edge Cases

The tests verify the happy path and basic failure, but consider adding tests for when clean_gemfile_lock is called with an empty array or when the lock file does not exist.

4. Documentation: Method Visibility

The clean_gemfile_lock method is currently private by context. Consider explicitly marking it as private for clarity.

Security Considerations

No security concerns identified

  • File operations are within the expected demo directory scope
  • No arbitrary path injection vulnerabilities
  • Uses safe file deletion (not shell commands)
  • No credential exposure

Performance Considerations

Minimal performance impact

  • Deleting a single file is negligible
  • Bundler will regenerate the lock file as part of normal install process
  • More efficient than attempting surgical edits to the lock file

Test Coverage Assessment

Good test coverage with minor suggestions above

  • Tests verify method is called with correct parameters
  • Both restore and swap scenarios covered
  • Error handling tested

Minor Observations

  1. The code correctly updates messages from Updating gems to Installing gems to match the new behavior. Nice attention to detail!
  2. Consistent use of clean terminology throughout. Good choice.

Overall Assessment

APPROVED

This is a well-thought-out fix that solves a real problem. The implementation is clean, safe, and well-tested. The minor suggestions above are optional enhancements rather than blockers.

Recommendation: Merge after considering the error handling suggestion for File.delete to make the tool more robust in edge cases.

Great work on the comprehensive test plan and clear documentation!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/demo_scripts/gem_swapper.rb (2)

1048-1057: Improve comment and error messaging clarity.

Two minor clarity issues:

  1. Line 1053: The comment "Install from scratch since we removed the lock file" could be clearer. bundle install without a lock file still respects version constraints in the Gemfile—it's not truly "from scratch" like a bundle update would be. Consider: "Regenerate lock file from Gemfile constraints".

  2. Line 1055: The error message "Check Gemfile for errors" is too specific. Bundle install failures can occur for many reasons (network issues, authentication, missing dependencies, etc.), not just Gemfile syntax errors.

Apply these changes for clearer messaging:

-          # Install from scratch since we removed the lock file
-          result = system('bundle', 'install', '--quiet')
-          warn '  ⚠️  ERROR: Failed to install gems. Check Gemfile for errors.' unless result
+          # Regenerate lock file from Gemfile constraints
+          result = system('bundle', 'install', '--quiet')
+          warn '  ⚠️  ERROR: Failed to install gems. Check bundle output for details.' unless result

1062-1070: Clarify "from scratch" comment.

Line 1068 has the same clarity issue as line 1053: the comment "Install from scratch since we removed the lock file" could be more precise. bundle install regenerates the lock file based on Gemfile constraints, which is different from a true "from scratch" approach.

Apply this change:

-        # Install from scratch since we removed the lock file
+        # Regenerate lock file from Gemfile constraints
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e45f1e5 and c014e36.

📒 Files selected for processing (1)
  • lib/demo_scripts/gem_swapper.rb (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (1)
lib/demo_scripts/gem_swapper.rb (1)

1197-1199: LGTM! Improved user guidance.

The expanded messaging provides clearer next steps by offering both manual and automated rebuild options, along with the command to view current paths. This enhances the user experience.

Comment on lines 1012 to 1027
# Removes stale gem entries from Gemfile.lock to prevent bundle update failures
# This is necessary when switching gem sources (rubygems <-> github <-> path)
# because the lock file may reference commits/versions that no longer exist
def clean_gemfile_lock(demo_path, gems_to_clean)
return unless gems_to_clean.any?

lock_file = File.join(demo_path, 'Gemfile.lock')
return unless File.exist?(lock_file)

puts " Cleaning lock file entries for: #{gems_to_clean.join(', ')}" if verbose

# Simply remove the lock file and let bundle regenerate it
# This is the safest approach to avoid any inconsistencies
File.delete(lock_file)
puts ' Removed Gemfile.lock to force clean resolution' if verbose
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Method name and behavior mismatch.

The method is named clean_gemfile_lock and accepts a gems_to_clean parameter, suggesting it would selectively clean specific gem entries. However, it removes the entire Gemfile.lock file regardless of which gems need cleaning (line 1025). This has implications:

  1. Unused parameter: The gems_to_clean parameter is only checked for emptiness but never used for selective cleaning.
  2. Broader impact: Deleting the entire lock file forces Bundler to regenerate all dependency versions, not just the problematic gems. This could inadvertently update unrelated dependencies if their version constraints allow it.
  3. Missing error handling: File.delete could fail (e.g., permission issues) without any error handling.

Consider these improvements:

Option 1 (simpler): Rename to reflect actual behavior:

-def clean_gemfile_lock(demo_path, gems_to_clean)
-  return unless gems_to_clean.any?
+def remove_gemfile_lock(demo_path)

Option 2 (more precise but complex): Implement selective cleaning by parsing and modifying Gemfile.lock to remove only the specified gem sections, preserving other dependency versions. This would require parsing the lock file format.

Add error handling:

-File.delete(lock_file)
+begin
+  File.delete(lock_file)
+rescue Errno::EACCES => e
+  warn "  ⚠️  Warning: Could not delete Gemfile.lock: #{e.message}"
+  return false
+end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Removes stale gem entries from Gemfile.lock to prevent bundle update failures
# This is necessary when switching gem sources (rubygems <-> github <-> path)
# because the lock file may reference commits/versions that no longer exist
def clean_gemfile_lock(demo_path, gems_to_clean)
return unless gems_to_clean.any?
lock_file = File.join(demo_path, 'Gemfile.lock')
return unless File.exist?(lock_file)
puts " Cleaning lock file entries for: #{gems_to_clean.join(', ')}" if verbose
# Simply remove the lock file and let bundle regenerate it
# This is the safest approach to avoid any inconsistencies
File.delete(lock_file)
puts ' Removed Gemfile.lock to force clean resolution' if verbose
end
# Removes stale gem entries from Gemfile.lock to prevent bundle update failures
# This is necessary when switching gem sources (rubygems <-> github <-> path)
# because the lock file may reference commits/versions that no longer exist
def remove_gemfile_lock(demo_path)
lock_file = File.join(demo_path, 'Gemfile.lock')
return unless File.exist?(lock_file)
puts " Cleaning lock file entries" if verbose
# Simply remove the lock file and let bundle regenerate it
# This is the safest approach to avoid any inconsistencies
begin
File.delete(lock_file)
puts ' Removed Gemfile.lock to force clean resolution' if verbose
rescue Errno::EACCES => e
warn " ⚠️ Warning: Could not delete Gemfile.lock: #{e.message}"
return false
end
end

The --status command now checks if GitHub-based swapped gems are outdated
and displays how many commits behind they are. This helps developers know
when they need to refresh their cached dependencies.

Features:
- Automatically fetches and compares commits with remote branch
- Shows number of commits behind for each outdated gem
- Provides example command to update

Example output:
  ⚠️  Outdated GitHub repositories detected:
     shakapacker (shakacode/shakapacker@justin808/early-hints) - 2 commit(s) behind

  💡 To update: Re-run your swap command to fetch latest changes
     Example: bin/swap-deps --github 'shakacode/shakapacker#justin808/early-hints'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 17, 2025

Code Review - PR #59: Fix GitHub ref swapping by cleaning stale Gemfile.lock entries

Overall Assessment

Approved with minor suggestions - This is a well-thought-out solution to a real problem. The approach of removing the Gemfile.lock before bundle operations when swapping gem sources is sound and aligns with Bundler best practices.


Code Quality & Best Practices

Strengths:

  • ✅ Clean, well-documented code with clear comments explaining the rationale
  • ✅ Follows Ruby conventions and repository RuboCop rules
  • ✅ Proper error handling with meaningful warning messages
  • ✅ Good separation of concerns with the new clean_gemfile_lock method
  • ✅ Consistent with existing codebase patterns

Suggestions:

  1. Method naming clarity (lib/demo_scripts/gem_swapper.rb:1072)
    The method clean_gemfile_lock might be better named remove_gemfile_lock since it completely removes the file rather than "cleaning" it. However, the current name is acceptable with the clear documentation.

  2. Verbose logging consistency (lib/demo_scripts/gem_swapper.rb:1078)
    Good use of the verbose flag for detailed logging. This follows the existing pattern in the codebase.


Potential Bugs & Issues

⚠️ Minor Concerns:

  1. Race condition edge case (lib/demo_scripts/gem_swapper.rb:1082)
    If File.delete(lock_file) succeeds but bundle install fails before creating a new lock file, the project is left without a Gemfile.lock. While the subsequent error message helps, consider:

    • Could add a backup/restore mechanism for the lock file
    • However, this is likely acceptable since the user can run bundle install manually
  2. Error message inconsistency (lib/demo_scripts/gem_swapper.rb:1112)
    The error message says "Failed to install gems. Check Gemfile for errors" but the issue might also be network-related, missing system dependencies, or other bundler issues. Consider making it more generic:

    warn '  ⚠️  ERROR: Failed to install gems. Check bundle output for details.' unless result
  3. Git fetch network failures (lib/demo_scripts/gem_swapper.rb:411)
    The check_commits_behind method silently swallows all errors. While this is appropriate for a status check, you might want to distinguish between:

    • Network failures (can't fetch)
    • Invalid branch names
    • Not a git repository

    Current behavior is safe but could be more informative in verbose mode.


Performance Considerations

✅ Good:

  • The clean_gemfile_lock method has early returns (return unless gems_to_clean.any?) preventing unnecessary file operations
  • File.exist? check before attempting deletion
  • Git operations properly use system with output suppression to avoid unnecessary I/O

New feature - Outdated repo checking:

  • The new check_outdated_repos functionality (lines 378-405) iterates through all swapped gems and performs git operations
  • For repos with many demos, this could add noticeable latency to --status command
  • Suggestion: Consider adding a flag like --check-updates to make this opt-in, or cache results for a short period (e.g., 5 minutes)

Security Concerns

✅ Secure:

  • ✅ No shell interpolation vulnerabilities - all system calls use array syntax: system('git', 'fetch', 'origin', branch, ...)
  • ✅ Proper escaping in regex patterns: Regexp.escape(gem_name)
  • ✅ Path handling uses safe methods like File.join and File.expand_path
  • ✅ No arbitrary code execution risks

Minor note:

  • The git fetch operation (line 411) fetches from 'origin' without validation. This is acceptable since the repo was cloned by the tool itself, so 'origin' is trusted.

Test Coverage

✅ Excellent test coverage:

  • All modified code paths have corresponding test cases
  • Tests properly verify the new clean_gemfile_lock method is called with correct parameters
  • Error handling paths are tested (lines 764-772)
  • Both restore and swap scenarios are covered

Suggestions:

  1. Add test for the new outdated repo feature:
    The new methods check_outdated_repos and check_commits_behind don't appear to have test coverage. Consider adding specs for:

    describe '#check_outdated_repos' do
      it 'detects repos behind remote'
      it 'returns empty array when repos are up to date'
      it 'handles git fetch failures gracefully'
    end
  2. Edge case test (spec/demo_scripts/gem_swapper_spec.rb:742):
    Consider adding a test for when File.delete succeeds but lock file doesn't exist (idempotent behavior).


Specific Code Review Comments

lib/demo_scripts/gem_swapper.rb:1082

File.delete(lock_file)

✅ Good: Simple and effective. Bundler will regenerate with correct entries.
💡 Consider: Could rescue Errno::ENOENT to make this more robust, though the File.exist? check above should prevent this.

lib/demo_scripts/gem_swapper.rb:378-419
The new outdated repo checking feature is a great addition for developer experience! The implementation is clean.

lib/demo_scripts/gem_swapper.rb:411

system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')

✅ Good: Proper suppression of output and safe system call.
💡 Consider: Might want to add a timeout for network operations to prevent hanging.

spec/demo_scripts/gem_swapper_spec.rb:785

expect(swapper).to receive(:clean_gemfile_lock).with(demo_path, %w[shakapacker react_on_rails])

✅ Excellent: Tests verify the new method is called with correct parameters.


Summary

This PR effectively solves a real pain point with gem source swapping. The solution is simple, well-tested, and follows best practices.

Recommendations:

  1. Merge as-is - The core functionality is solid
  2. 💡 Consider follow-up: Add tests for the new outdated repo detection feature
  3. 💡 Consider follow-up: Make outdated repo checking opt-in or cached for performance
  4. 💡 Minor: Update error message to be more generic (line 1112)

Test Results: As noted in the PR description, 77/86 tests pass with 6 pre-existing failures unrelated to this change - this is acceptable.

Great work on identifying and fixing this issue! 🎉


Review generated by Claude Code - feel free to discuss any points above!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/demo_scripts/gem_swapper.rb (1)

1069-1084: Past review feedback remains unaddressed.

The issues raised in the previous review comment still apply:

  1. Unused parameter: gems_to_clean is only checked for emptiness but never used for selective cleaning
  2. Method behavior mismatch: The method removes the entire Gemfile.lock regardless of which gems need cleaning
  3. Missing error handling: File.delete at line 1082 could fail without any error handling

The previous review suggested either:

  • Renaming to remove_gemfile_lock to reflect actual behavior
  • Implementing selective cleaning (complex)
  • Adding error handling for the file deletion

Consider at minimum adding error handling:

-File.delete(lock_file)
-puts '  Removed Gemfile.lock to force clean resolution' if verbose
+begin
+  File.delete(lock_file)
+  puts '  Removed Gemfile.lock to force clean resolution' if verbose
+rescue Errno::EACCES => e
+  warn "  ⚠️  Warning: Could not delete Gemfile.lock: #{e.message}"
+  return false
+end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c014e36 and 93cd90b.

📒 Files selected for processing (1)
  • lib/demo_scripts/gem_swapper.rb (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/demo_scripts/gem_swapper.rb (2)
lib/demo_scripts/swap_deps_cli.rb (1)
  • each_demo (509-517)
lib/demo_scripts/demo_manager.rb (1)
  • each_demo (19-30)
🪛 Brakeman (7.1.0)
lib/demo_scripts/gem_swapper.rb

[medium] 414-414: Possible command injection
Type: Command Injection
Confidence: Medium
More info: https://brakemanscanner.org/docs/warning_types/command_injection/

(Command Injection)

🔇 Additional comments (4)
lib/demo_scripts/gem_swapper.rb (4)

141-157: Well-implemented outdated repository detection!

The integration of outdated repository checks in show_status is clear and provides actionable guidance to users. The example command at line 156 is particularly helpful.


377-405: LGTM - Clean implementation!

The method correctly identifies outdated GitHub repositories by checking commit differences. The logic flow is clear with appropriate early returns using next.


1087-1138: Good integration of lock file cleaning.

The changes correctly integrate clean_gemfile_lock before bundle operations and appropriately replace bundle update with bundle install after removing the lock file. The updated error messaging at line 1112 is also clearer.

The logic properly handles three scenarios:

  1. Restore with no swapped gems (lines 1097-1103)
  2. Restore with swapped gems requiring lock cleanup (lines 1105-1115)
  3. Swap with lock cleanup (lines 1119-1127)

Note: This inherits any issues from clean_gemfile_lock if it fails to delete the lock file (see separate comment).


1254-1257: Improved user guidance!

The updated messaging provides clearer instructions about rebuild options, showing both the manual path to check status and the automatic rebuild option with the --watch flag. This improves the user experience.

Comment on lines 407 to 419
# Check how many commits behind the local cache is from the remote branch
def check_commits_behind(cache_path, branch)
Dir.chdir(cache_path) do
# Fetch latest without output
system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')

# Count commits between local and remote
output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
return status.success? ? output.strip.to_i : nil
end
rescue StandardError
nil
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid string interpolation in git commands.

Line 414 interpolates branch directly into a git command string, which is flagged by Brakeman as a potential command injection risk. While validate_github_branch should provide some protection, it's safer to use parameterized commands consistently.

Apply this diff to use the array form:

     Dir.chdir(cache_path) do
       # Fetch latest without output
       system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
 
       # Count commits between local and remote
-      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
+      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}", chdir: cache_path)
       return status.success? ? output.strip.to_i : nil
     end

Wait, the issue is that Open3.capture2 requires the command as separate arguments, but we're passing a string. Let me revise:

     Dir.chdir(cache_path) do
       # Fetch latest without output
       system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
 
       # Count commits between local and remote
-      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
+      output, status = Open3.capture2('git', 'rev-list', '--count', 'HEAD', "--not", "origin/#{branch}")
       return status.success? ? output.strip.to_i : nil
     end

Actually, git's rev-list syntax requires the range format. The safest approach is:

     Dir.chdir(cache_path) do
       # Fetch latest without output
       system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
 
+      remote_ref = "origin/#{branch}"
+      # Validate the remote ref exists before using it
+      _output, status = Open3.capture2('git', 'rev-parse', '--verify', remote_ref)
+      return nil unless status.success?
+
       # Count commits between local and remote
-      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
+      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..#{remote_ref}")
       return status.success? ? output.strip.to_i : nil
     end

This adds validation while still using the necessary string interpolation for the ref range. The key improvement is verifying the ref exists first, which provides defense-in-depth beyond the initial branch validation.

🧰 Tools
🪛 Brakeman (7.1.0)

[medium] 414-414: Possible command injection
Type: Command Injection
Confidence: Medium
More info: https://brakemanscanner.org/docs/warning_types/command_injection/

(Command Injection)

🤖 Prompt for AI Agents
In lib/demo_scripts/gem_swapper.rb around lines 407 to 419, replace the
Open3.capture2 call that currently builds a single command string with the
array/argument form so git is invoked without shell interpolation, and add a
prior check that the remote ref exists (e.g., git show-ref --verify --quiet
refs/remotes/origin/<branch> using Open3.capture3 or system) returning nil if
the ref is missing; then call Open3.capture2('git', 'rev-list', '--count',
"HEAD..origin/#{branch}") as separate args and preserve the existing
status.success? handling and rescue behavior.

The issue was that swap-deps was requiring bundler/setup unconditionally,
which would fail when running from a demo directory with a Gemfile that
had GitHub gems not yet checked out.

Solution: Only require bundler/setup for commands that actually need gem
dependencies (swap, restore, apply). Read-only commands (status, cache,
list-watch, kill-watch, clean-cache) only use git/filesystem operations
and don't need bundler.

This fixes the error:
  'The git source https://github.com/... is not yet checked out.
   Please run `bundle install`...'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 17, 2025

Pull Request Review: Fix GitHub ref swapping by cleaning stale Gemfile.lock entries

Summary

This PR addresses a critical issue with gem swapping where stale Gemfile.lock entries cause bundle failures when switching between gem sources. The solution is well-architected and solves the root cause by removing the lock file before bundle operations.


✅ Strengths

1. Clean Architecture & Separation of Concerns

  • The clean_gemfile_lock method (lib/demo_scripts/gem_swapper.rb:1069-1084) is well-designed with a single, clear responsibility
  • Good documentation explaining why lock file removal is necessary
  • Proper guard clauses prevent unnecessary operations

2. Comprehensive Test Coverage

  • Tests updated to verify the new clean_gemfile_lock behavior
  • All test expectations properly adjusted from bundle update to bundle install
  • Tests cover both restore and swap scenarios (spec/demo_scripts/gem_swapper_spec.rb:734-790)

3. Enhanced User Experience

  • New outdated repository detection feature provides proactive feedback
  • Clear, actionable guidance for users when gems are outdated
  • Improved help messages with both manual and automatic rebuild options

4. Code Quality

  • Follows repository conventions (RuboCop compliant)
  • Consistent with existing code style
  • Good use of conditional execution (verbose mode, file existence checks)

🔍 Observations & Considerations

1. Security & Safety
lib/demo_scripts/gem_swapper.rb:1082

  • Observation: File.delete(lock_file) permanently removes the lock file
  • Impact: Low risk - this is intentional behavior, and the lock file is regenerated
  • Consideration: Users who have local modifications to Gemfile.lock will lose them
  • Recommendation: Consider documenting this behavior in user-facing messages

2. Performance Consideration
lib/demo_scripts/gem_swapper.rb:408-419

def check_commits_behind(cache_path, branch)
  Dir.chdir(cache_path) do
    # Fetch latest without output
    system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
  • Observation: The show_status command now performs git fetches for all swapped GitHub gems
  • Impact: Status command may be slower, especially with multiple repos or slow network
  • Recommendation: Consider adding a flag like --check-updates to make this optional, or cache fetch results with a TTL

3. Error Handling
lib/demo_scripts/gem_swapper.rb:408-419

  • Good: Rescue block catches errors gracefully and returns nil
  • Consideration: Silent failures might hide network or permission issues
  • Recommendation: Consider logging failures in verbose mode for debugging

4. Edge Case: Concurrent Operations
lib/demo_scripts/gem_swapper.rb:1082

  • Observation: File deletion doesn't have race condition protection
  • Impact: Minimal - tool is typically run by single user
  • Note: Current implementation is acceptable for the use case

5. Bundler Behavior Change
lib/demo_scripts/gem_swapper.rb:1103-1123

  • Change: Switched from bundle update to bundle install
  • Impact: Positive - more deterministic and safer
  • Justification: Since lock file is removed, install from scratch is correct approach

🐛 Potential Issues

Minor: Array Access Without Guard
lib/demo_scripts/gem_swapper.rb:156

puts "   Example: bin/swap-deps --github '#{outdated_repos.first[:repo]}##{outdated_repos.first[:ref]}'"
  • Issue: Accesses .first after outdated_repos.any? check, which is safe
  • Severity: Low - logic is correct, but could be more defensive
  • Suggestion: No change needed, but could use &.first for extra safety

Minor: Read-only Command Detection
lib/demo_scripts/swap_deps_cli.rb:88-90

def read_only_command?
  @show_status || @show_cache || @list_watch || @kill_watch || @clean_cache || @clean_cache_gem
end
  • Observation: @show_status now performs git fetches (not purely read-only)
  • Impact: Minimal - bundler/setup isn't needed for git operations
  • Note: Naming might be slightly misleading but functionally correct

🧪 Test Coverage Assessment

Excellent Coverage:

  • ✅ Tests verify clean_gemfile_lock is called with correct parameters
  • ✅ Tests updated to expect bundle install instead of bundle update
  • ✅ Error handling tested (spec/demo_scripts/gem_swapper_spec.rb:761-773)
  • ✅ Both restore and swap paths tested

Missing Tests (Non-blocking):

  • New check_outdated_repos method lacks direct unit tests
  • check_commits_behind method lacks tests
  • Recommendation: Add tests in a follow-up PR to verify outdated detection logic

📋 Best Practices Compliance

Code Style: Follows RuboCop guidelines
Documentation: Well-commented, clear intent
Git Practices: Clean commit with proper attribution
Testing: Tests updated to match implementation
Error Handling: Appropriate guards and fallbacks
User Experience: Clear messages and guidance


🔒 Security Assessment

No Security Concerns Identified

  • File operations are safe and scoped to project directories
  • Git operations use safe, read-only fetches
  • No credential exposure or sensitive data handling
  • Command injection protected by proper argument passing (array form)

🎯 Recommendations

Priority: Low - These are suggestions for future enhancements, not blockers

  1. Documentation: Add a comment explaining the performance implications of status checks
  2. Performance: Consider making git fetch optional via flag (--check-updates)
  3. Testing: Add unit tests for the new outdated detection methods in a follow-up
  4. Logging: Add verbose mode logging for git fetch failures to aid debugging

✨ Final Assessment

Recommendation: ✅ APPROVE

This is a high-quality PR that solves a real problem with a clean, well-tested solution. The code is production-ready and follows all repository best practices.

Key Wins:

  • Solves the root cause (stale lock file references)
  • Adds valuable user-facing features (outdated detection)
  • Maintains backward compatibility
  • Excellent test coverage
  • Clear, maintainable code

No blocking issues identified. The minor observations above are suggestions for potential future enhancements, not requirements for merging.

Great work! 🚀


Review generated with Claude Code

Previously, swap-deps had to be run from the project root. Now it can be
run from anywhere inside the project and it will:

1. Automatically find the project root by walking up the directory tree
   looking for .swap-deps.yml or bin/swap-deps
2. Change to the project root before running
3. Detect if you were in a demo directory and auto-scope to that demo

This makes it much more convenient to use:
- Run from demo directory: auto-scopes to that demo
- Run from nested subdirectory: still finds project root and works
- Run from project root: works as before

Example output when run from demos/basic-v16-rspack:
  📍 Changed to project root: /path/to/project
     Detected you were in demo: basic-v16-rspack
     Auto-scoped to this demo only

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
lib/demo_scripts/swap_deps_cli.rb (1)

87-90: Consider renaming for clarity (optional).

The method name read_only_command? might be slightly misleading since it includes operations that modify state (e.g., kill_watch kills processes, clean_cache deletes files). However, the semantic intent—"commands that don't require gem dependencies"—is clear from the context and comment.

For improved clarity, consider alternative names:

  • skip_bundler_setup?
  • command_without_gem_dependencies?
  • standalone_command?

That said, the current name works well enough given the comment on line 43 explains the intent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93cd90b and dff7ad3.

📒 Files selected for processing (1)
  • lib/demo_scripts/swap_deps_cli.rb (2 hunks)
🔇 Additional comments (1)
lib/demo_scripts/swap_deps_cli.rb (1)

42-44: Verification complete: the conditional bundler/setup optimization is safe and correct.

Both DependencySwapper and FilteredDependencySwapper can be instantiated without bundler/setup. Their initialize methods only call super(**options) and set instance variables—no gem loading occurs. All file-level requires in gem_swapper.rb are standard library. The optimization properly defers bundler/setup loading until needed, which is correct for read-only commands (status, cache, watch operations).

When --status detects outdated GitHub repositories, users can now add
the --auto-update flag to automatically pull latest changes and rebuild
npm packages.

Usage:
  bin/swap-deps --status --auto-update

This will:
1. Check all swapped GitHub repos for new commits
2. Automatically update any that are behind
3. Rebuild their npm packages
4. Report what was updated

Example output:
  ⚠️  Outdated GitHub repositories detected:
     shakapacker (shakacode/shakapacker@justin808/early-hints) - 6 commit(s) behind

  🔄 Auto-updating outdated repositories...
    Updating shakapacker...
    Rebuilding shakapacker...

  ✅ Updated 1 repository/repositories
  💡 Changes are now available in your demos

The --status command now also suggests using --auto-update when
outdated repos are detected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 17, 2025

Pull Request Review - PR #59

Summary

This PR addresses a critical issue with gem swapping by cleaning stale Gemfile.lock entries when switching between gem sources. The approach is pragmatic and effective, with good test coverage updates.


✅ Strengths

  1. Clear Problem-Solution Fit: The issue is well-defined (stale lock file references causing bundle failures), and the solution is straightforward (remove lock file and regenerate).

  2. Good Documentation: Excellent inline comments explaining why clean_gemfile_lock is necessary, particularly in lib/demo_scripts/gem_swapper.rb:1069-1071.

  3. Test Coverage: Tests were properly updated to reflect the new behavior, changing from bundle update to bundle install after lock file cleanup.

  4. User Experience Improvements:

    • Outdated GitHub repository detection (lib/demo_scripts/gem_swapper.rb:377-419)
    • Better post-action guidance for rebuilds
    • Optimized read_only_command? to skip bundler/setup for status/cache commands
  5. Project Root Detection: The refactored detect_context! method (lib/demo_scripts/swap_deps_cli.rb:162-185) is more robust with proper directory traversal.


🔍 Issues & Concerns

1. Potential Race Condition in check_commits_behind (lib/demo_scripts/gem_swapper.rb:408-419)

Severity: Medium

The system() call doesn't check return status. If fetch fails silently, rev-list will use stale data.

Recommendation: Check fetch success before counting commits

2. Command Injection Risk in check_commits_behind (lib/demo_scripts/gem_swapper.rb:414)

Severity: High 🔴

String interpolation of branch into git command without validation. If branch name contains special characters, it could lead to command injection.

Recommendation: Validate branch names to ensure they don't contain unexpected characters.

3. Missing Test Coverage for New Features

Severity: Medium

The following new methods lack test coverage:

  • check_outdated_repos (lib/demo_scripts/gem_swapper.rb:378)
  • check_commits_behind (lib/demo_scripts/gem_swapper.rb:408)
  • clean_gemfile_lock (lib/demo_scripts/gem_swapper.rb:1072)
  • read_only_command? (lib/demo_scripts/swap_deps_cli.rb:88)
  • find_project_root (lib/demo_scripts/swap_deps_cli.rb:188)
  • detect_demo_from_path (lib/demo_scripts/swap_deps_cli.rb:207)

Recommendation: Add unit tests, especially for clean_gemfile_lock and check_commits_behind with mocked git commands.

4. Performance: Git Fetch on Every Status Check

Severity: Low

system('git', 'fetch', ...) is called for every swapped GitHub gem during status checks. For multiple gems, this could be slow.

Recommendation: Consider adding a --skip-fetch flag or caching fetch results with a timestamp.


🔒 Security Assessment

  1. Command Injection Risk: See issue Add Claude Code GitHub Workflow #2 above - needs attention
  2. File Deletion: File.delete(lock_file) is safe as path is constructed internally
  3. Dir.chdir: Used safely with blocks that restore original directory
  4. Git operations: Using system() with array args is generally safe, but validate branch names

⚡ Performance Considerations

  1. Lock File Deletion: Forcing full bundle install is slower than bundle update, but necessary for correctness. Trade-off is acceptable.
  2. Network calls: Git fetch on status checks could add latency
  3. Directory traversal: find_project_root has max_depth=10 limit, which is good

🧪 Test Coverage Summary

Modified Tests: 3 specs updated in spec/demo_scripts/gem_swapper_spec.rb

  • ✅ Tests updated to expect clean_gemfile_lock call
  • ✅ Changed from bundle update to bundle install expectations
  • ✅ Error messages updated

Missing Tests: 6+ new methods without coverage


📋 Recommendations Summary

  1. High Priority:

  2. Medium Priority:

  3. Low Priority:

    • Improve error messages with more context
    • Consider caching/throttling git fetch operations

✨ Overall Assessment

Code Quality: Good - well-structured, readable, with clear intent

Approach: Pragmatic - deleting lock file is simpler and safer than parsing/editing it

Risk Level: Medium - command injection concern needs addressing before merge

Recommendation: Request changes - address the command injection issue in check_commits_behind, then this is good to merge.

Great work on solving a tricky bundler issue! The approach is sound and will save developers from frustrating Could not find gem errors.


Review generated with Claude Code

Auto-updating outdated repos is now enabled by default when running
--status, making it more convenient for users. They no longer need to
remember to add --auto-update.

Changes:
- Auto-update defaults to true
- Added --no-auto-update flag to disable if needed
- Updated help text to show it's the default
- Simplified success message

Usage:
  bin/swap-deps --status              # Auto-updates by default
  bin/swap-deps --status --no-auto-update  # Skip auto-update

This provides the best user experience - outdated repos are automatically
refreshed without requiring extra flags or manual intervention.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 17, 2025

Code Review - PR #59: Fix GitHub ref swapping by cleaning stale Gemfile.lock entries

Summary

This PR addresses a real pain point when swapping gem sources by removing stale Gemfile.lock entries. The implementation is solid with good test coverage and clear documentation.


Strengths

1. Problem-Solution Fit

  • Clearly addresses the core issue: stale lock file references causing "Could not find gem" errors
  • Simple, effective solution: delete and regenerate the lock file
  • Good inline documentation explaining the rationale (lib/demo_scripts/gem_swapper.rb:1111-1113)

2. Code Quality

  • Clean separation of concerns with clean_gemfile_lock method
  • Comprehensive test updates that match the implementation changes
  • Helpful verbose logging for debugging
  • RuboCop compliant

3. Enhanced Features

  • Outdated repo detection (lib/demo_scripts/gem_swapper.rb:385-411): Automatically detects when GitHub caches are behind
  • Auto-update capability: New --auto-update flag for seamless updates
  • Smart context detection (lib/demo_scripts/swap_deps_cli.rb:163-218): Finds project root intelligently
  • Performance optimization: read_only_command? skips bundler/setup when not needed

@claude
Copy link

claude bot commented Oct 17, 2025


Issues & Recommendations

🔴 High Priority

1. Race Condition Risk in clean_gemfile_lock (lib/demo_scripts/gem_swapper.rb:1114-1126)

The method deletes Gemfile.lock but does not verify the bundle install succeeds before proceeding. If the install fails, you have lost the lock file with no rollback.

Recommendation: Create a backup before deletion and restore it if the bundle install fails.

2. Silent Failure in check_commits_behind (lib/demo_scripts/gem_swapper.rb:416-427)

Returns nil on any error, making debugging difficult. Users will not know if the check failed vs. being up-to-date. Add warning messages for verbose mode.

🟡 Medium Priority

3. Directory Traversal Without Validation (lib/demo_scripts/swap_deps_cli.rb:189-205)

The find_project_root method walks up the directory tree but does not validate against symlink attacks or circular references. Use File.realpath and track visited directories.

4. Dir.chdir Without Error Handling (lib/demo_scripts/swap_deps_cli.rb:172)

Changing directory in detect_context! could fail or leave the process in an unexpected state. Wrap in begin/rescue block.

5. Inconsistent Error Messaging

The second bundle install call (line 1165) should also check for errors and provide guidance like the first one does.

6. Test Coverage Gap

The tests mock file operations but do not test error cases or the new features (check_outdated_repos, update_outdated_repos, auto-update flag, context detection).

@claude
Copy link

claude bot commented Oct 17, 2025


Security Considerations

No major security issues found

  • File operations use proper path joining (File.join)
  • No command injection risks (uses array form of system)
  • No credential exposure
  • Appropriate use of File.realpath would further harden path traversal

Performance Considerations

Good optimizations:

  • read_only_command? check avoids loading bundler unnecessarily
  • Git fetch with redirected output (out: '/dev/null')
  • Efficient regex compilation note in comments

⚠️ Potential concern:

  • update_outdated_repos runs npm run build serially for each gem. For multiple gems, this could be slow. Consider parallel builds.

Test Coverage

Strengths:

  • Updated tests reflect the new clean_gemfile_lock + bundle install flow
  • Tests verify the change from bundle update to bundle install
  • Proper mocking of file system operations

⚠️ Gaps:

  • No tests for check_outdated_repos, check_commits_behind, update_outdated_repos
  • No tests for the new --auto-update flag
  • No tests for the improved context detection (find_project_root, detect_demo_from_path)
  • No error case testing for file deletion failures

Recommendation: Add integration-style tests or at least unit tests for the new outdated repo detection feature.


Overall Assessment

Rating: 8/10 - Strong implementation with minor areas for improvement

Recommendation: Approve with suggestions

The core fix is sound and solves the stated problem effectively. The additional features (outdated detection, context detection) are valuable additions. Addressing the high-priority error handling issues would make this production-ready.

Before merging:

  1. Add error handling for clean_gemfile_lock (backup/restore)
  2. Consider adding tests for the new outdated repo detection features
  3. Add error logging to check_commits_behind for easier debugging

Great work on a thorough solution to a real developer pain point! 🚀


Review generated by Claude Code

@claude
Copy link

claude bot commented Oct 17, 2025

Code Review: Fix GitHub ref swapping by cleaning stale Gemfile.lock entries

Summary

This PR addresses a real pain point when swapping gem sources between rubygems, GitHub, and local paths. The solution of removing Gemfile.lock before installation is pragmatic and effective. Overall, this is a well-implemented fix with good test coverage.

✅ Strengths

1. Core Solution is Sound

  • The clean_gemfile_lock method (lib/demo_scripts/gem_swapper.rb:1114) correctly identifies the root cause
  • Removing the lock file and regenerating it is the safest approach
  • Good separation of concerns with a dedicated method

2. Enhanced User Experience

  • Outdated repo detection proactively identifies when cached GitHub repos are behind
  • Auto-update capability with --auto-update flag
  • Improved guidance for next steps

3. Context Detection Improvements

  • find_project_root and detect_demo_from_path intelligently detect demo directory context
  • Auto-scoping to current demo is a nice UX improvement

4. Performance Optimization

  • read_only_command? skips bundler/setup for commands that don't need it
  • Avoids unnecessary gem loading for status/cache/watch commands

5. Test Coverage

  • Tests updated to reflect new behavior
  • Tests verify lock file cleaning and bundle install

⚠️ Issues & Concerns

1. Potential Race Condition in check_commits_behind (line 418)

Issue: system() returns boolean, cannot check if fetch succeeded. If fetch fails silently, rev-list will use stale refs.

Recommendation: Check fetch_status before proceeding with rev-list

2. Silent Failure in update_github_repo (line 438)

Issue: update_github_repo return value not checked. Users won't know if update fails.

Recommendation: Check return value and warn on failure

3. Error Message Confusion (lines 1148, 1163)

Issue: Message says Check Gemfile for errors but actual problem could be network, dependencies, etc.

Recommendation: Use more generic message like Check output above for details

4. NPM Build Exception Handling (line 451)

Issue: Rescue only catches JSON::ParserError, not other Dir.chdir or system exceptions

Recommendation: Add File.directory? check or broaden rescue scope

🔍 Code Quality

Positive:

  • Good RuboCop usage
  • Clear method responsibilities
  • Good keyword arguments
  • Consistent patterns
  • Descriptive names

Areas for improvement:

  • Some methods getting long (update_outdated_repos 29 lines)
  • show_status has I/O and side effects - consider separation

🚀 Performance

Good:

  • read_only_command? optimization
  • Git quiet flags
  • Caching with cache_repo_dirs

Concerns:

  • check_outdated_repos runs git fetch per gem - could batch
  • Lock file deletion forces full resolution - consider surgical approach

🔒 Security

Good:

  • No arbitrary command execution
  • Safe file operations
  • Git operations use array arguments

Minor concerns:

  • Git fetch assumes trusted origin
  • npm run build executes package.json scripts (general npm issue)

🧪 Test Coverage

Good:

  • Tests updated for new behavior
  • Tests verify clean_gemfile_lock calls
  • Error path testing

Gaps:

  • No tests for check_outdated_repos
  • No tests for check_commits_behind
  • No tests for update_outdated_repos
  • No tests for find_project_root/detect_demo_from_path

🎯 Recommendations

Must Fix:

  1. Check git fetch return value in check_commits_behind
  2. Check update_github_repo return value

Should Fix:
3. Add tests for outdated repo detection features
4. Improve error messages
5. Add directory check before Dir.chdir in npm build

Nice to Have:
6. Extract npm rebuild logic
7. Add YARD documentation
8. Batch git fetch operations

✅ Conclusion

This is a solid PR solving a real problem effectively. Core fix is right, additional features are valuable.

Main concerns:

  • Error handling around git operations
  • Test coverage for new features
  • Minor robustness improvements

Recommendation: Approve with minor revisions

Great work improving the gem swapping developer experience!


Review generated by Claude Code

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/demo_scripts/gem_swapper.rb (1)

1134-1156: Logging mismatch: message says “bundle update” but we run install.

Fix user-visible text to match behavior.

Apply:

-        puts '  Running bundle update (to restore gem sources)...'
+        puts '  Running bundle install (to restore gem sources)...'

If you rename clean_gemfile_lock per prior comment, update call sites too:

-          clean_gemfile_lock(demo_path, gems_to_update)
+          remove_gemfile_lock(demo_path)

And in the swap branch:

-        clean_gemfile_lock(demo_path, gems_to_update)
+        remove_gemfile_lock(demo_path)

Also applies to: 1147-1169

♻️ Duplicate comments (2)
lib/demo_scripts/gem_swapper.rb (2)

415-427: Harden git ref usage and avoid false positives from missing remote refs.

Add a remote ref existence check and avoid Dir.chdir for safety; also use File::NULL for portability. This was previously flagged; reinforcing here.

As per static analysis hints (Brakeman), apply:

-  def check_commits_behind(cache_path, branch)
-    Dir.chdir(cache_path) do
-      # Fetch latest without output
-      system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
-
-      # Count commits between local and remote
-      output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
-      return status.success? ? output.strip.to_i : nil
-    end
-  rescue StandardError
-    nil
-  end
+  def check_commits_behind(cache_path, branch)
+    # Fetch latest without output
+    system('git', '-C', cache_path, 'fetch', 'origin', branch, out: File::NULL, err: File::NULL)
+
+    remote_ref = "origin/#{branch}"
+    # Verify remote ref exists
+    _out, status = Open3.capture2('git', '-C', cache_path, 'rev-parse', '--verify', remote_ref)
+    return nil unless status.success?
+
+    # Count commits between local and remote
+    out, status = Open3.capture2('git', '-C', cache_path, 'rev-list', '--count', "HEAD..#{remote_ref}")
+    status.success? ? out.strip.to_i : nil
+  rescue StandardError
+    nil
+  end

Optional: also guard branch validation earlier or normalize ref_type to branch-only before calling this method.

#!/bin/bash
# Verify there are no remaining uses of '/dev/null' redirections and search for similar git rev-list patterns.
rg -n --glob '!**/vendor/**' "/dev/null|HEAD\.\.origin/" -C2

1111-1126: Method name/param mismatch and missing error handling (already noted earlier).

  • The method deletes the entire lock file irrespective of gems_to_clean.
  • Unused gems_to_clean misleads callers.
  • No error handling on File.delete.

Apply either rename or implement selective cleaning; minimally, add error handling:

-def clean_gemfile_lock(demo_path, gems_to_clean)
-  return unless gems_to_clean.any?
+def remove_gemfile_lock(demo_path)
   lock_file = File.join(demo_path, 'Gemfile.lock')
   return unless File.exist?(lock_file)
-  puts "  Cleaning lock file entries for: #{gems_to_clean.join(', ')}" if verbose
+  puts '  Removing Gemfile.lock to force clean resolution' if verbose
-  File.delete(lock_file)
-  puts '  Removed Gemfile.lock to force clean resolution' if verbose
+  begin
+    File.delete(lock_file)
+    puts '  ✓ Removed Gemfile.lock' if verbose
+  rescue SystemCallError => e
+    warn "  ⚠️  Warning: Could not delete Gemfile.lock: #{e.message}"
+    return false
+  end
 end

And update call sites (see below comment) accordingly.

🧹 Nitpick comments (2)
lib/demo_scripts/swap_deps_cli.rb (1)

163-186: Minor portability nits in path handling (optional).

Using simple string prefix/sub may misbehave under symlinks or on Windows. Prefer Pathname.realpath + relative_path, and File::SEPARATOR-agnostic logic. Example sketch:

  • Use Pathname.new(current_path).realpath.relative_path_from(Pathname.new(project_root).realpath)
  • Use File::NULL instead of '/dev/null' when redirecting (cross‑platform)

No behavior change needed now; consider for robustness.

Also applies to: 188-206, 207-219

lib/demo_scripts/gem_swapper.rb (1)

385-413: Skip tag-based refs in “outdated” check; capture ref_type from Gemfile.

Counting “commits behind” only makes sense for branches. Current detection doesn’t distinguish branch vs tag, so tags may be treated as branches. Recommend:

  • Capture whether the Gemfile entry used branch or tag.
  • Skip commits-behind for tags.

Apply these diffs outside this hunk to capture ref_type and use it here:

-    GEM_GITHUB_PATTERN = lambda { |gem_name|
-      github_pattern = /^\s*gem\s+["']#{Regexp.escape(gem_name)}["'],\s*github:\s*["']([^"']+)["']/
-      github_ref_pattern = /(?:,\s*(?:branch|tag):\s*["']([^"']+)["'])?/
-      /#{github_pattern}#{github_ref_pattern}/
-    }
+    GEM_GITHUB_PATTERN = lambda { |gem_name|
+      github_pattern = /^\s*gem\s+["']#{Regexp.escape(gem_name)}["'],\s*github:\s*["']([^"']+)["']/
+      # capture 1: repo, 2: ref_type (branch|tag), 3: ref value
+      github_ref_pattern = /(?:,\s*(branch|tag):\s*["']([^"']+)["'])?/
+      /#{github_pattern}#{github_ref_pattern}/
+    }
-        github_match = gemfile_content.match(GEM_GITHUB_PATTERN.call(gem_name))
+        github_match = gemfile_content.match(GEM_GITHUB_PATTERN.call(gem_name))
...
-        elsif github_match
-          ref = github_match[2] || 'main'
-          swapped_gems << { name: gem_name, type: 'github', path: "#{github_match[1]}@#{ref}" }
+        elsif github_match
+          repo = github_match[1]
+          ref_type = (github_match[2] || 'branch').to_sym
+          ref = github_match[3] || 'main'
+          swapped_gems << { name: gem_name, type: 'github', path: "#{repo}@#{ref}", ref_type: ref_type }

Then in this method:

-      swapped_gems.each do |gem|
+      swapped_gems.each do |gem|
         next unless gem[:type] == 'github'
+        # Only branches can be "behind"
+        next if gem[:ref_type] == :tag
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dff7ad3 and 5674136.

📒 Files selected for processing (2)
  • lib/demo_scripts/gem_swapper.rb (5 hunks)
  • lib/demo_scripts/swap_deps_cli.rb (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/demo_scripts/gem_swapper.rb (2)
lib/demo_scripts/swap_deps_cli.rb (1)
  • each_demo (555-563)
lib/demo_scripts/demo_manager.rb (1)
  • each_demo (19-30)
lib/demo_scripts/swap_deps_cli.rb (1)
lib/demo_scripts/gem_swapper.rb (1)
  • show_status (139-165)
🪛 Brakeman (7.1.0)
lib/demo_scripts/gem_swapper.rb

[medium] 422-422: Possible command injection
Type: Command Injection
Confidence: Medium
More info: https://brakemanscanner.org/docs/warning_types/command_injection/

(Command Injection)

🔇 Additional comments (4)
lib/demo_scripts/swap_deps_cli.rb (3)

13-13: Auto-update plumbing looks good.

attr_reader, default true, CLI flag wiring, and propagation to show_status are consistent.

Also applies to: 35-35, 322-325, 487-490


43-46: Safe gating of bundler/setup for read-only commands.

Good separation; avoids unnecessary boot time for status/cache/watch-only flows.

Also applies to: 88-92


1296-1299: Updated next-steps messaging: LGTM.

lib/demo_scripts/gem_swapper.rb (1)

138-165: Status + auto-update flow: LGTM.

Clear output; dedupes repos; conditional auto-update reads CLI flag.

Addresses code review feedback with critical safety improvements:

1. Fixed race condition in clean_gemfile_lock:
   - Now creates backup before deleting lock file
   - Restores backup if bundle install fails
   - Cleans up backup on success
   - Prevents loss of working lock file state

2. Added verbose warnings to check_commits_behind:
   - Warns when git fetch fails
   - Warns when commit counting fails
   - Provides context for debugging
   - Helps distinguish errors from being up-to-date

3. Added error handling for Dir.chdir:
   - Wrapped in begin/rescue block
   - Raises clear error message on failure
   - Prevents process from being stuck in wrong directory

4. Validated directory traversal in find_project_root:
   - Uses File.realpath to resolve symlinks
   - Tracks visited directories to detect circular references
   - Catches and handles SystemCallError
   - Protects against symlink attacks

These changes make the tool more robust and production-ready, with
proper error handling and recovery mechanisms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link

claude bot commented Oct 18, 2025

Code Review - PR #59

Thank you for this comprehensive PR! The implementation addresses a real pain point with gem dependency swapping and includes several valuable features.


Summary

This PR successfully fixes the "Could not find gem" errors when swapping between different gem sources by removing stale Gemfile.lock entries. The solution is solid, with good error handling and backup mechanisms added in the latest commit.


Strengths

1. Excellent Safety Improvements

The backup/restore mechanism in clean_gemfile_lock is well-designed:

  • Creates backup before deletion
  • Restores on failure
  • Cleans up on success
  • Prevents data loss

2. Good Error Handling

  • Proper exception handling in check_commits_behind
  • Clear error messages with context
  • Graceful degradation when operations fail

3. Security Considerations

  • Directory traversal protection in find_project_root (lib/demo_scripts/swap_deps_cli.rb:196)
  • File.realpath to resolve symlinks
  • Circular reference detection with visited set
  • Max depth limit prevents infinite loops

4. Great UX Features

  • Auto-update outdated repos by default
  • Clearer next steps messaging
  • Ability to run from any directory within project

5. Test Coverage

Tests were properly updated to reflect the new flow


Issues & Recommendations

Critical: Race Condition Risk

Location: lib/demo_scripts/gem_swapper.rb:1133-1142

Issue: Potential race condition between creating the backup and deleting the lock file.

Recommendation: Add error handling to file operations to make the process more robust.

Minor: Incomplete Error Handling

Location: lib/demo_scripts/gem_swapper.rb:1135, 1150, 1158

Issue: File operations can fail (permission denied, disk full) but aren't wrapped in error handling.

Recommendation: Wrap FileUtils.cp, File.delete, FileUtils.mv, and FileUtils.rm in begin/rescue blocks.

Minor: Missing Test Coverage

What's missing:

  1. Tests for clean_gemfile_lock, restore_lock_backup, cleanup_lock_backup
  2. Tests for check_outdated_repos and update_outdated_repos
  3. Tests for find_project_root edge cases
  4. Tests for detect_demo_from_path

Performance: Redundant System Calls

Location: lib/demo_scripts/gem_swapper.rb:419-426

Issue: check_commits_behind fetches on every check.

Recommendation: Consider caching fetch results for ~5 minutes.


Security Assessment

Good practices:

  • Symlink resolution prevents directory traversal
  • Circular reference detection
  • Array form of system (not string interpolation)

Consider:

  • Git branch names are user-controlled
  • Auto-update pulls code from GitHub (acceptable for dev tool)

Test Coverage Assessment

Current:

  • Updated existing tests to match new behavior
  • Tests properly mock clean_gemfile_lock
  • Missing tests for new features

Recommended:

  1. Test backup/restore scenarios
  2. Test directory traversal edge cases
  3. Test outdated repo detection
  4. Test error handling paths

Verdict

Recommendation: Approve with minor suggestions

This PR is well-implemented with good safety mechanisms. The issues noted are minor and can be addressed in follow-up PRs.

Priority:

  1. High: Add error handling to file operations
  2. Medium: Add tests for new methods
  3. Low: Add documentation, performance optimizations

Great work! This will significantly improve the developer experience.

@justin808 justin808 merged commit 4ff9c4f into main Oct 18, 2025
1 of 2 checks passed
@justin808 justin808 deleted the fix-github-refs branch October 18, 2025 07:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/demo_scripts/gem_swapper.rb (1)

1169-1196: Log message says “bundle update” but code runs install.

Align the message with actual command to avoid confusion.

-        puts '  Running bundle update (to restore gem sources)...'
+        puts '  Running bundle install (to restore gem sources)...'
♻️ Duplicate comments (3)
lib/demo_scripts/gem_swapper.rb (3)

416-433: Harden git rev-list call; verify remote ref first.

Validate ref characters, ensure remote ref exists, then run rev-list. Addresses Brakeman warning and tag/invalid ref cases.

-    def check_commits_behind(cache_path, branch)
+    def check_commits_behind(cache_path, branch)
+      # Basic sanity check to avoid unsafe ref usage
+      return nil unless branch.is_a?(String) && branch.match?(/\A[\w.\-\/]+\z/)
       Dir.chdir(cache_path) do
         # Fetch latest without output
         fetch_success = system('git', 'fetch', 'origin', branch, out: '/dev/null', err: '/dev/null')
         unless fetch_success
           warn "  ⚠️  Warning: Failed to fetch branch #{branch} from #{cache_path}" if verbose
           return nil
         end
-
-        # Count commits between local and remote
-        output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..origin/#{branch}")
+        remote_ref = "origin/#{branch}"
+        # Verify remote ref exists (skips tags or missing refs)
+        _out, status = Open3.capture2('git', 'rev-parse', '--verify', remote_ref)
+        return nil unless status.success?
+        # Count commits between local and remote
+        output, status = Open3.capture2('git', 'rev-list', '--count', "HEAD..#{remote_ref}")
         unless status.success?
           warn "  ⚠️  Warning: Failed to count commits for #{branch} in #{cache_path}" if verbose
           return nil
         end

1121-1161: Method name vs behavior; add error handling.

The method deletes the entire lockfile regardless of gems passed; param is unused beyond logging, and failures aren’t handled.

Option A — rename to reflect behavior and add error handling:

-    # Removes stale gem entries from Gemfile.lock to prevent bundle update failures
-    # Returns the backup path if created, nil otherwise
-    def clean_gemfile_lock(demo_path, gems_to_clean)
+    # Removes Gemfile.lock to force a clean resolution. Returns backup path or nil.
+    def remove_gemfile_lock(demo_path, gems_to_clean = nil)
       return nil unless gems_to_clean.any?
@@
-      FileUtils.cp(lock_file, backup_lock)
+      begin
+        FileUtils.cp(lock_file, backup_lock)
+      rescue SystemCallError => e
+        warn "  ⚠️  Warning: Could not back up Gemfile.lock: #{e.message}"
+        return nil
+      end
@@
-      File.delete(lock_file)
+      begin
+        File.delete(lock_file)
+      rescue SystemCallError => e
+        warn "  ⚠️  Warning: Could not delete Gemfile.lock: #{e.message}"
+        return backup_lock
+      end

And update call sites:

-          lock_backup = clean_gemfile_lock(demo_path, gems_to_update)
+          lock_backup = remove_gemfile_lock(demo_path, gems_to_update)
@@
-        lock_backup = clean_gemfile_lock(demo_path, gems_to_update)
+        lock_backup = remove_gemfile_lock(demo_path, gems_to_update)

Option B — keep name but document and still add error handling (if rename is undesirable).


385-413: Skip “behind” checks for tags and validate ref from Gemfile.

Gemfile lines don’t preserve branch vs tag; treating a tag like a branch will fail fetch/rev-list. Add a remote-ref existence check and bail early if not a branch ref.

-        commits_behind = check_commits_behind(cache_path, ref)
+        commits_behind = check_commits_behind(cache_path, ref)
         next unless commits_behind&.positive?

And within check_commits_behind (below), verify origin/<ref> exists; tags will naturally be skipped.

🧹 Nitpick comments (4)
lib/demo_scripts/swap_deps_cli.rb (3)

163-191: Use realpath for current_dir to match project_root resolution.

Prevents false negatives when cwd is a symlink.

-      current_dir = Dir.pwd
+      current_dir = File.realpath(Dir.pwd) rescue Dir.pwd

199-223: Depth limiter may miss project roots in deep trees.

Limit of 10 can fail in monorepos. Prefer loop-to-root with visited set.

-      max_depth = 10 # Prevent infinite loops
-
-      max_depth.times do
+      loop do
         # Check if we've seen this directory before (circular reference)
         return nil if visited.include?(dir)
@@
-        break if parent == dir # Reached filesystem root
+        break if parent == dir # Reached filesystem root

224-236: Harden demo detection against symlinks.

Compare realpaths to avoid misclassification.

-    def detect_demo_from_path(current_path, project_root)
-      return nil unless current_path.start_with?(project_root)
+    def detect_demo_from_path(current_path, project_root)
+      current_real = File.realpath(current_path) rescue current_path
+      return nil unless current_real.start_with?(project_root)
-      relative_path = current_path.sub("#{project_root}/", '')
+      relative_path = current_real.sub("#{project_root}/", '')
lib/demo_scripts/gem_swapper.rb (1)

139-165: Status with auto-update — looks good.

Clear summary and guarded updates. Consider sorting by commits_behind desc for readability later.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5674136 and 0223a54.

📒 Files selected for processing (2)
  • lib/demo_scripts/gem_swapper.rb (6 hunks)
  • lib/demo_scripts/swap_deps_cli.rb (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/demo_scripts/gem_swapper.rb (2)
lib/demo_scripts/swap_deps_cli.rb (1)
  • each_demo (572-580)
lib/demo_scripts/demo_manager.rb (1)
  • each_demo (19-30)
lib/demo_scripts/swap_deps_cli.rb (1)
lib/demo_scripts/gem_swapper.rb (1)
  • show_status (139-165)
🪛 Brakeman (7.1.0)
lib/demo_scripts/gem_swapper.rb

[medium] 426-426: Possible command injection
Type: Command Injection
Confidence: Medium
More info: https://brakemanscanner.org/docs/warning_types/command_injection/

(Command Injection)

🔇 Additional comments (9)
lib/demo_scripts/swap_deps_cli.rb (6)

13-13: Expose auto_update via attr_reader — OK.

Public reader aligns with CLI flag usage and downstream propagation.


35-35: Default auto_update = true — confirm desired default behavior.

This will mutate caches on --status by default. If that’s intended, fine; otherwise consider defaulting to false.


43-46: Lazy require for Bundler — good separation.

Avoids unnecessary env mutation for read-only commands.


88-91: read_only_command? scope looks right.

Covers status/cache/watch flows that don’t need gems.


339-342: --[no-]auto-update flag — OK.

Option wiring and default messaging look good.


506-507: Propagate auto_update to swapper — OK.

Signature matches downstream.

lib/demo_scripts/gem_swapper.rb (3)

439-471: Auto-update flow — OK.

Good use of cache update, optional rebuilds, and robust JSON handling.


1203-1209: Install path after lock removal — OK.

Installing from scratch post-delete is appropriate.


1343-1346: Next steps messaging — OK.

Helpful guidance; integrates with status feature.

Comment on lines +194 to +201
# Find the project root by walking up the directory tree
def find_project_root(start_dir = Dir.pwd)
# Resolve symlinks to prevent attacks and circular references
dir = File.realpath(start_dir)
visited = Set.new
max_depth = 10 # Prevent infinite loops

max_depth.times do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

NameError risk: Set is used but not required.

find_project_root uses Set without require 'set'. This will crash at runtime.

 require 'optparse'
+require 'set'

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/demo_scripts/swap_deps_cli.rb around lines 194 to 201, the method
find_project_root uses Set but the file doesn’t require 'set', which will raise
a NameError at runtime; fix by adding require 'set' at the top of the file (or
immediately before find_project_root) so Set is defined before use, ensuring the
require is only added once and keeping require ordering consistent with existing
requires in this file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants