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
diffto 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'sDefNode. Cutting fromdeftoendwith a regex is impossible because nestedends 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}/underGem.path).upstream_diffresolves the same path for two versions — relying onbundle updateleaving the old version's source undergems/— and diffs them with the same extraction pipeline. - Version check: Reads
Gemfile.lockwithBundler::LockfileParserand compares against the marker's version. TheGemfile.lockpath 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