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 #2063

Closed
wants to merge 24 commits into from

Conversation

alexcrocha
Copy link
Contributor

WIP

This Draft PR is still a work in progress.

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

To address this, a snapshot file of Gemfile.lock is created to persist data across server restarts. Upon restart, both the snapshot and the current Gemfile.lock are parsed using Bundler::LockfileParser. If differences are detected, the relevant gem names and specifications are extracted, allowing us to then trigger the gem RBI generation.

This implementation handles gem additions, spec changes, and gem removals.

Tests

@alexcrocha alexcrocha added the enhancement New feature or request label Oct 31, 2024
@vinistock
Copy link
Member

Independent of the code changes themselves, I still have doubts about the UX here. Take this scenario for example:

  1. You are working on a branch developing some feature
  2. Now, you need to rebase your branch on the latest main
  3. The latest main has lockfile changes, but those weren't accompanied by the necessary RBI updates
  4. After the rebase, the LSP will immediately generate gem RBIs that are completely unrelated to your work, resulting in untracked changes

I believe we need to try to come up with a strategy to be able to tell if the current developer made the update to the lockfile vs it being updated by something else (like git pull/switching branches/rebasing). Otherwise, it will result in a pretty annoying experience to constantly get unrelated changes.

Maybe, we could remember the launch timestamp of the add-on. When the lockfile is updated, it will trigger a restart and then the add-on can check if the update to the lockfile happened after the previous launch (which should indicate that the current developer made the update). I don't know, there might be better options on how to achieve this.

@alexcrocha
Copy link
Contributor Author

@vinistock, thanks for pointing this out. I agree that the current behaviour could quickly become frustrating, especially when rebasing and getting unrelated gem RBIs.

I appreciate your suggestion about using the launch timestamp of the add-on to distinguish between lockfile updates made by the developer and those from other actions. I'll experiment with that idea and explore other strategies. Your feedback has given me a lot to think about.

Thanks again for your input!

vinistock and others added 2 commits November 4, 2024 11:52
Do not reload rake tasks when under add-on environment
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.
Skip gem RBI generation by comparing the last modified times of the
Gemfile and Gemfile.lock. If both files are modified within a fraction
of a second of each other, it is likely due to a git operation, and gem
RBI generation can be skipped.
@alexcrocha alexcrocha force-pushed the ar/gem-regeneration branch 2 times, most recently from 898a5aa to 52045e7 Compare November 9, 2024 00:58
If the Ruby LSP was stopped shortly after the last git checkout/pull
operation, we don't need to regenerate RBIs since any Gemfile.lock
changes were likely from version control, not from bundle install
return unless File.exist?(".git") && File.exist?(".ruby-lsp/shutdown-timestamp")

git_timestamp = fetch_last_git_operation_time
return unless git_timestamp
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this isn't right.

Let's say you're working on a new repo, where the reflog doesn't contain any checkout or pull entries. fetch_last_git_operation_time will return nil, and so we will never reach process_gemfile_changes.

@@ -1760,7 +1760,7 @@ def self.gather_constants

it "generates RBIs for lower versions of activerecord-typedstore" do
@project.require_real_gem("activerecord-typedstore", "1.4.0")
@project.require_real_gem("sqlite3", "1.7.3")
@project.require_real_gem("sqlite3", "~> 2.1")
Copy link
Contributor

Choose a reason for hiding this comment

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

(looks like something was incorrectly rebased?)


sig { returns(T.nilable(Time)) }
def fetch_last_git_operation_time
git_reflog_output = %x(git reflog --date=iso | grep -E "checkout|pull" | head -n 1).strip
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
git_reflog_output = %x(git reflog --date=iso | grep -E "checkout|pull" | head -n 1).strip
git_reflog_output = %x(git reflog --date=iso --grep-reflog='^\(checkout\|pull\)' -1).strip

This approach has a couple of advantages:

  • It helps prevent mismatches due to a commit messages co-incidentally containing the word 'checkout' or 'pull'. (The ^ means match only at the start of the line).
  • It avoids the need for external programs which may not be available on Windows.


sig { void }
def handle_gemfile_changes
return unless File.exist?(".git") && File.exist?(".ruby-lsp/shutdown-timestamp")
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's extract this path to a constant since we refer to it multiple times.

@@ -19,6 +19,9 @@ module Tapioca
class Addon < ::RubyLsp::Addon
extend T::Sig

GEMFILE_LOCK_SNAPSHOT = "tmp/tapioca/.gemfile_lock_snapshot"
GIT_OPERATION_THRESHOLD = 15.0 # seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
GIT_OPERATION_THRESHOLD = 15.0 # seconds
GIT_OPERATION_THRESHOLD = 15 # seconds

return if git_reflog_output.empty?

timestamp_string = T.must(git_reflog_output.match(/\{(.*)\}/))[1]
Time.iso8601(T.must(timestamp_string).sub(" ", "T").delete(" "))
Copy link
Contributor

Choose a reason for hiding this comment

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

We could avoid the need for this parsing by setting the output so it contains only the timestamp:

git reflog ..... --format=tformat:'%cI'

(cI means strict ISO, so we don't need to add the 'T' ourselves).

@alexcrocha
Copy link
Contributor Author

Closed in favour of #2081

@alexcrocha alexcrocha closed this Nov 26, 2024
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.

4 participants