Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: [Tapioca Addon] Support gem RBI generation on dirty lockfile #2081

Draft
wants to merge 4 commits into
base: tapioca-addon-feature-branch
Choose a base branch
from

Conversation

alexcrocha
Copy link

@alexcrocha alexcrocha commented Nov 20, 2024

WIP

This Draft PR is still a work in progress and an alternative implementation to #2063

Motivation

To support gem RBI generation, we needed a way to detect changes in Gemfile.lock. Currently, changes to this file cause the Ruby LSP to restart, resulting in loss of access to any previous state information. This limitation prevents monitoring and processing of gem changes.

Implementation

We've implemented a solution that uses git diff to track Gemfile.lock changes:

  1. Change Detection

    • Added git_repo? check to ensure we're in a git repository
    • Use git diff HEAD Gemfile.lock to detect uncommitted changes
    • Parse the diff output to identify gem removals, additions or modifications
  2. Gem Change Analysis

    • Created LockfileDiffParser class to analyze diff output
    • Identifies:
      • Removed gems (completely deleted)
      • Added or modified gems (new or version changes)
  3. Gem RBI Processing

    • Remove RBI files for deleted gems
    • Triggers gem rbis generation command for added or modified gems

Key Components

  • Addon: Monitors Gemfile.lock changes and notifies the ServerAddon when changes are detected
  • LockfileDiffParser: Analyzes git diff output to identify added, modified, and removed gems
  • ServerAddon: Triggers the execution of the actual RBI file operations (removal or generation)

This approach specifically targets manual bundle updates while avoiding unnecessary RBI regeneration during normal git operations like branch switches.

Previous Implementation

To address this, we check git status to determine if Gemfile.lock has uncommitted changes. When changes are detected, we:

1. Compare the current Gemfile.lock with a snapshot stored in tmp/tapioca/.gemfile_lock_snapshot
2. Use Bundler::LockfileParser to parse both files and detect:
- Removed gems (present in snapshot but not in current)
- Changed gems (version differences between snapshot and current)
3. Process the changes in the server addon:
- Remove RBI files for deleted gems
- Generate new RBI files for changed gems using Tapioca
4. Update the snapshot with the current lockfile content

Tests

@alexcrocha alexcrocha added the enhancement New feature or request label Nov 20, 2024
lib/ruby_lsp/tapioca/addon.rb Outdated Show resolved Hide resolved
lib/ruby_lsp/tapioca/server_addon.rb Outdated Show resolved Hide resolved
lib/ruby_lsp/tapioca/addon.rb Outdated Show resolved Hide resolved
current_lockfile = File.read("Gemfile.lock")
snapshot_lockfile = File.read(GEMFILE_LOCK_SNAPSHOT) if File.exist?(GEMFILE_LOCK_SNAPSHOT)

unless snapshot_lockfile
Copy link
Contributor

Choose a reason for hiding this comment

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

Have we compared the snapshot approach to git diff? I can think of one scenario where you update the lockfile before snapshot is created so upon the consequent restart there are changes in git status however, snapshot doesn't exist yet so we don't generate. It's not a common scenario so not important. But feels like running git diff instead is a viable solution. Curious about the reasoning here. Does it have downsides for changing branches?

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for mentioning git diff again. I looked into it with more attention and I do agree it is a much nicer solution.

I have pushed a refactor to take advantage of git diff. Let me know what you think.

def handle_gemfile_changes
return unless File.exist?(".git")

gemfile_status = %x(git status --porcelain Gemfile.lock).strip
Copy link
Contributor

Choose a reason for hiding this comment

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

With shelling out and the file parsing I'm curious how long this adds to the boot time. Could you add some simple timings to the PR description, before and after this change for core?

Copy link
Author

Choose a reason for hiding this comment

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

This is now an older implementation, but I do agree some timings would be nice to have. I'll take a look on Monday.

To support gem RBI generation, we needed a way to detect changes in
Gemfile.lock. Currently, changes to this file cause the Ruby LSP to
restart, resulting in loss of access to any previous state information.

By creating a snapshot of Gemfile.lock, we can persist data across
server restarts. Upon restart, we parse both the snapshot and current
Gemfile.lock using Bundler::LockfileParser. If differences are found,
we extract the relevant gem names and specifications, allowing us to
trigger the gem RBI generation.
Only regenerate RBIs when Gemfile.lock has uncommitted changes in git,
which targets manual bundle updates. This improves performance by avoiding
unnecessary regeneration during normal git operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants