gem_override_marker

English | 日本語

When you want to customize a method or view inside a gem, you often copy the upstream method or view and edit just the parts you need. The problem: when the gem is upgraded and the original method or view has changed, you won't notice, and that silently becomes a bug.

gem_override_marker gives you a way to efficiently re-apply your customization to the new version when a gem is upgraded.

Tutorial

Suppose a gem greeter has a method Greeter::Message#hello.

# Upstream code of greeter 1.0.0:
# gems/greeter-1.0.0/lib/greeter/message.rb
module Greeter
  class Message
    def hello(name)
      "Hello, #{name}."
    end
  end
end

1. Customize it, and write a marker

Say you want the greeting in Japanese. You reopen the class and override hello (a monkey patch). At this point you write a one-line marker declaring which gem, which version, and what you based it on.

# app/overrides/greeter_patch.rb
# @gem-override greeter-1.0.0/lib/greeter/message.rb#hello
# @see https://github.com/your-org/your-repo/issues/42
module Greeter
  class Message
    def hello(name)
      "こんにちは、#{name}さん。"
    end
  end
end

2. Check that the marker is recognized

$ rake gem_override_marker:list
app/overrides/greeter_patch.rb:2	greeter-1.0.0/lib/greeter/message.rb#hello

1 marker(s)

You can also see the diff against upstream (i.e. what you changed) right away.

$ rake gem_override_marker:diff
--- source greeter-1.0.0/lib/greeter/message.rb#hello
+++ override app/overrides/greeter_patch.rb#hello
@@ -1,3 +1,3 @@
 def hello(name)
-  "Hello, #{name}."
+  "こんにちは、#{name}さん。"
 end

3. The gem gets upgraded

Suppose greeter 2.0.0 is released and the upstream hello implementation has changed.

# Upstream code of greeter 2.0.0 (implementation changed):
module Greeter
  class Message
    def hello(name)
      greeting = "Hello"
      "#{greeting}, #{name}! Welcome."
    end
  end
end

You bump Gemfile to 2.0.0 and run bundle update. Your customization is still based on 1.0.0, so upstream changes like ! Welcome. are not reflected.

4. See "what you changed in the old version"

Even after bundle update, the 1.0.0 source remains under gems/. Since the marker still points to greeter-1.0.0, diff shows upstream 1.0.0 vs your customization — that is, exactly what you changed.

$ rake gem_override_marker:diff
--- source greeter-1.0.0/lib/greeter/message.rb#hello
+++ override app/overrides/greeter_patch.rb#hello
@@ -1,3 +1,3 @@
 def hello(name)
-  "Hello, #{name}."
+  "こんにちは、#{name}さん。"
 end

→ You can see that all you did was "make the greeting Japanese".

5. Re-apply that change onto the new upstream

Take upstream 2.0.0's new implementation (the greeting variable and the added Welcome.) as the base, and re-apply only your change (the Japanese greeting). Also update the marker version to 2.0.0.

# app/overrides/greeter_patch.rb
# @gem-override greeter-2.0.0/lib/greeter/message.rb#hello
# @see https://github.com/your-org/your-repo/issues/42
module Greeter
  class Message
    def hello(name)
      greeting = "こんにちは"
      "#{greeting}#{name}さん!ようこそ。"
    end
  end
end

Finally, confirm that the diff against the new upstream is as intended (just the Japanese greeting).

$ rake gem_override_marker:diff
--- source greeter-2.0.0/lib/greeter/message.rb#hello
+++ override app/overrides/greeter_patch.rb#hello
@@ -1,4 +1,4 @@
 def hello(name)
-  greeting = "Hello"
-  "#{greeting}, #{name}! Welcome."
+  greeting = "こんにちは"
+  "#{greeting}、#{name}さん!ようこそ。"
 end

Upstream 2.0.0's Welcome. is preserved, and only your "Japanese greeting" shows up as the diff. You have successfully kept up with the upgrade.

Design philosophy: re-apply "your diff" onto the new upstream

Steps 4–5 of the tutorial are the heart of this tool. The diff it provides is only "upstream vs your override", and that is by design — it assumes you work this way:

  • Review only the diff you added on top of upstream, then re-apply it onto the new upstream
  • ❌ NOT "merge the diff between old and new upstream into your override"

The latter is painful: if upstream has been heavily refactored, a huge amount of unrelated diff shows up. All you need to know is "what you changed", and re-applying that onto the new base is far easier. Even if you have 10 overrides, you only look at the spots you touched — you don't have to read the entire upstream changelog.

What makes this possible is the fact that bundle update leaves the old version's source under gems/. As long as the marker points to the old version, you can regenerate "old upstream vs your override" even after installing the new version, and use it as a guide while re-applying onto the new version.

Triaging which overrides upstream actually changed (upstream_diff)

When many gems are upgraded at once, re-applying every override blindly is wasteful: some overrides target a spot upstream didn't touch at all, so all you need to do there is bump the marker version. upstream_diff tells the two groups apart by diffing the two upstream versions (old → new) for each marker's target — not upstream vs your override.

$ rake gem_override_marker:upstream_diff
========================================================================
app/overrides/greeter_patch.rb:2  greeter-1.0.0/lib/greeter/message.rb#hello
========================================================================
--- upstream greeter-1.0.0/lib/greeter/message.rb#hello
+++ upstream greeter-2.0.0/lib/greeter/message.rb#hello
@@ -1,3 +1,4 @@
 def hello(name)
-  "Hello, #{name}."
+  greeting = "Hello"
+  "#{greeting}, #{name}! Welcome."
 end
  • No change ((no upstream change)) → upstream didn't touch this spot, so leave the override code alone and just bump the marker version (e.g. 1.0.0 → 2.0.0).
  • Changed → use diff to review what you changed, then re-apply it onto the new upstream as in step 5, and bump the marker version.

The target version defaults to the one in Gemfile.lock (the upgraded version). Pass to_version to compare against a specific version instead. This keeps the design philosophy intact — it does not merge upstream's diff into your override; it only helps you decide where re-applying is even needed.

Detecting version drift (check)

If you forget to update the marker version after re-applying, the version the marker points to (old) and the actual version in Gemfile.lock (new) will drift. check detects this drift and exits with code 1 if found (so it can run in CI). It is the watchdog that keeps markers from being "set and forgotten".

$ rake gem_override_marker:check
❌ Marker versions differ from Gemfile.lock (possible outdated overrides):
  app/overrides/greeter_patch.rb:2
    marker version: greeter-1.0.0
    locked version: greeter-2.0.0

Writing markers

Declare it on one line, right before the overridden method / at the top of the overriding file.

# @gem-override spree_core-5.1.6/app/models/spree/stock/packer.rb#default_package
def default_package
  # ...
end

For a whole-file override of a view / partial, omit the method name; the entire file is the target.

<%# @gem-override spree_storefront-5.1.6/app/views/spree/products/_cart_form.html.erb %>

The marker format is {gem}-{version}/{path-in-gem}#{method}. Because it includes the gem name, even when a single repository ships multiple gems (e.g. Spree), "which gem's version to compare against" is unambiguous.

The tool only uses the @gem-override line. You can freely add notes on the following lines — why you made the override, which issue it relates to, etc. (the tool ignores them). To leave a pointer to an issue, the YARD @see tag works well.

# @gem-override spree_core-5.1.6/app/models/spree/stock/packer.rb#default_package
# @see https://github.com/your-org/your-repo/issues/1187
def default_package
  # ...
end

Only lines that start with "comment marker + exactly one space + @gem-override" are treated as valid markers. This avoids false positives from indented examples in documentation or quoted mentions in prose.

Command reference

# List markers
rake gem_override_marker:list

# Diff upstream vs override (compared against the marker's version)
rake gem_override_marker:diff

# Diff two upstream versions for each marker (old → Gemfile.lock version, or a given to_version)
rake gem_override_marker:upstream_diff
rake gem_override_marker:upstream_diff[,2.0.0]

# Detect drift between marker version and Gemfile.lock (exits 1 on mismatch)
rake gem_override_marker:check

Without arguments, list / diff / upstream_diff / check target every marker in the repository. They also accept a selector argument to target specific markers.

# (1) override file path -> all markers in the file
rake gem_override_marker:diff[app/overrides/greeter_patch.rb]

# (2) path + method name -> just that method
rake gem_override_marker:diff[app/overrides/greeter_patch.rb,hello]

# (3) the marker's logical name -> a single unambiguous target
rake gem_override_marker:diff[greeter-1.0.0/lib/greeter/message.rb#hello]

How it works

For reference, the internals are as follows.

  • Method extraction: Parses the source into an AST with prism (standard in Ruby 3.3) and slices only the target method's DefNode. Cutting from def to end with a regex is impossible because nested ends can't be distinguished, so a parser is used.
  • Indent normalization: Upstream (directly under a class) and the override (nested in modules) differ in nesting depth, so the slice is dedented to absorb the difference.
  • Diff: Generates a unified diff with diff-lcs.
  • Upstream source resolution: Resolved from the actual gem install path (gems/{gem}-{version}/ under Gem.path). upstream_diff resolves the same path for two versions — relying on bundle update leaving the old version's source under gems/ — and diffs them with the same extraction pipeline.
  • Version check: Reads Gemfile.lock with Bundler::LockfileParser and compares against the marker's version. The Gemfile.lock path is resolved automatically by Bundler.

Since these only analyze upstream and the override as text, they do not depend on Rails' :environment; they run without booting DB connections or app initialization.

License

MIT