gem_override_marker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.ja.md +302 -0
- data/README.md +316 -0
- data/gem_override_marker.gemspec +33 -0
- data/lib/gem_override_marker/diff_generator.rb +40 -0
- data/lib/gem_override_marker/gem_source.rb +75 -0
- data/lib/gem_override_marker/marker.rb +88 -0
- data/lib/gem_override_marker/railtie.rb +11 -0
- data/lib/gem_override_marker/registry.rb +76 -0
- data/lib/gem_override_marker/source_extractor.rb +69 -0
- data/lib/gem_override_marker/version.rb +3 -0
- data/lib/gem_override_marker.rb +79 -0
- data/lib/tasks/gem_override_marker.rake +89 -0
- data/spec/fixtures/Gemfile.lock +14 -0
- data/spec/fixtures/Gemfile.lock.mismatch +14 -0
- data/spec/fixtures/override_module.rb +12 -0
- data/spec/fixtures/repo/no_marker.rb +8 -0
- data/spec/fixtures/repo/sub/another.erb +3 -0
- data/spec/fixtures/repo/with_marker.rb +7 -0
- data/spec/fixtures/source_class.rb +14 -0
- metadata +117 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "diff/lcs"
|
|
2
|
+
require "diff/lcs/hunk"
|
|
3
|
+
|
|
4
|
+
module GemOverrideMarker
|
|
5
|
+
# 本家ソースと override の unified diff を生成する。
|
|
6
|
+
class DiffGenerator
|
|
7
|
+
# 前後に表示するコンテキスト行数。
|
|
8
|
+
CONTEXT_LINES = 3
|
|
9
|
+
|
|
10
|
+
# 2つのソース文字列の unified diff を返す。差分が無ければ空文字。
|
|
11
|
+
def self.unified(old_source, new_source, old_label:, new_label:)
|
|
12
|
+
old_lines = old_source.lines.map(&:chomp)
|
|
13
|
+
new_lines = new_source.lines.map(&:chomp)
|
|
14
|
+
|
|
15
|
+
changes = Diff::LCS.diff(old_lines, new_lines)
|
|
16
|
+
return "" if changes.empty?
|
|
17
|
+
|
|
18
|
+
output = +"--- #{old_label}\n+++ #{new_label}\n"
|
|
19
|
+
# diff-lcs の Hunk はコンテキスト行ぶんのオフセットを file_length_difference として
|
|
20
|
+
# hunk 間で累積する必要がある。これを引き継がず 0 固定にすると、離れた hunk 同士で
|
|
21
|
+
# 内部オフセットが破綻して @data_old[lo..hi] が範囲外 (nil) になりクラッシュする。
|
|
22
|
+
# コンテキストが重なる hunk は直前の hunk に merge し、重ならなければ flush して出力する。
|
|
23
|
+
file_length_difference = 0
|
|
24
|
+
last_hunk = nil
|
|
25
|
+
changes.each do |hunk_changes|
|
|
26
|
+
hunk = Diff::LCS::Hunk.new(old_lines, new_lines, hunk_changes, CONTEXT_LINES, file_length_difference)
|
|
27
|
+
file_length_difference = hunk.file_length_difference
|
|
28
|
+
|
|
29
|
+
if last_hunk && hunk.merge(last_hunk)
|
|
30
|
+
# merge 済み (overlaps した) ので last_hunk は出力せず hunk に取り込まれている
|
|
31
|
+
elsif last_hunk
|
|
32
|
+
output << last_hunk.diff(:unified) << "\n"
|
|
33
|
+
end
|
|
34
|
+
last_hunk = hunk
|
|
35
|
+
end
|
|
36
|
+
output << last_hunk.diff(:unified, true) << "\n" if last_hunk
|
|
37
|
+
output
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module GemOverrideMarker
|
|
2
|
+
# gem のインストールパス解決と、Gemfile.lock 上の実バージョン取得を担う。
|
|
3
|
+
#
|
|
4
|
+
# diff の比較対象となる本家ソースは、`{gem名}-{バージョン}/{gem内パス}` を
|
|
5
|
+
# 実環境の gem インストールパス (Gem.path 配下の gems/) から解決する。
|
|
6
|
+
# マーカーに書かれたバージョンを正とするが、アップグレード検討時などは
|
|
7
|
+
# 明示的に別バージョンを指定できる。
|
|
8
|
+
class GemSource
|
|
9
|
+
class SourceNotFoundError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# マーカー対象ファイル (本家ソース) の絶対パスを返す。
|
|
12
|
+
# 比較対象の版はマーカーに書かれた版を使う。
|
|
13
|
+
def self.resolve_file(marker)
|
|
14
|
+
resolve_path(marker.gem_name, marker.version, marker.path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# gem名・版・gem内パスから本家ソースの絶対パスを返す。
|
|
18
|
+
# マーカーに縛られず任意の版を解決できるため、本家2版間の diff
|
|
19
|
+
# (upstream_diff) で from / to 双方の本家ソースを得るのに使う。
|
|
20
|
+
def self.resolve_path(gem_name, version, path)
|
|
21
|
+
dir = gem_dir(gem_name, version)
|
|
22
|
+
unless dir
|
|
23
|
+
raise SourceNotFoundError,
|
|
24
|
+
"#{gem_name}-#{version} not found in gem install paths"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
file = File.join(dir, path)
|
|
28
|
+
unless File.exist?(file)
|
|
29
|
+
raise SourceNotFoundError, "gem source not found: #{file}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
file
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# {gem名}-{バージョン} の gem ディレクトリを Gem.path 配下から探す。
|
|
36
|
+
# 見つからなければ nil。
|
|
37
|
+
def self.gem_dir(gem_name, version)
|
|
38
|
+
gem_paths.each do |base|
|
|
39
|
+
candidate = File.join(base, "gems", "#{gem_name}-#{version}")
|
|
40
|
+
return candidate if Dir.exist?(candidate)
|
|
41
|
+
end
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Gemfile.lock 上で実際に解決されている gem のバージョンを返す。
|
|
46
|
+
# マーカーの版とこの版がズレていれば追従漏れの可能性がある (check で使う)。
|
|
47
|
+
#
|
|
48
|
+
# lockfile_path を省略すると Bundler が解決したプロジェクトの Gemfile.lock を
|
|
49
|
+
# 使う。Bundler は BUNDLE_GEMFILE やカレントからの探索で Gemfile の場所を
|
|
50
|
+
# 自動解決するため、利用側はパスを意識しなくてよい。
|
|
51
|
+
def self.locked_version(gem_name, lockfile_path = nil)
|
|
52
|
+
path = lockfile_path || default_lockfile_path
|
|
53
|
+
return nil if path.nil? || !File.exist?(path)
|
|
54
|
+
|
|
55
|
+
require "bundler"
|
|
56
|
+
parser = Bundler::LockfileParser.new(File.read(path))
|
|
57
|
+
spec = parser.specs.find { |s| s.name == gem_name }
|
|
58
|
+
spec&.version&.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Bundler が解決したプロジェクトの Gemfile.lock の絶対パス。
|
|
62
|
+
# Bundler が使えない (非 Bundler 環境) 場合はカレントの Gemfile.lock を試す。
|
|
63
|
+
def self.default_lockfile_path
|
|
64
|
+
require "bundler"
|
|
65
|
+
Bundler.default_lockfile.to_s
|
|
66
|
+
rescue StandardError
|
|
67
|
+
File.expand_path("Gemfile.lock")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Gem.path を基本とし、現在ロード中の Gem.dir も補う。
|
|
71
|
+
def self.gem_paths
|
|
72
|
+
([ Gem.dir ] + Gem.path).uniq
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module GemOverrideMarker
|
|
2
|
+
# `@gem-override` マーカー1行を解析した結果を表す値オブジェクト。
|
|
3
|
+
#
|
|
4
|
+
# gem のメソッドを prepend で丸ごと再実装したり、gem の view / partial を
|
|
5
|
+
# コピーして丸ごと上書きしている箇所に、本家のどこを基準にしているかを
|
|
6
|
+
# 宣言するためのマーカー。decorator メソッドの直前 / 上書きファイルの冒頭に置く。
|
|
7
|
+
#
|
|
8
|
+
# @gem-override {gem名}-{バージョン}/{gem内パス}#{メソッド名}
|
|
9
|
+
#
|
|
10
|
+
# 例:
|
|
11
|
+
# @gem-override spree_core-5.1.6/app/models/spree/stock/packer.rb#default_package
|
|
12
|
+
# @gem-override spree_storefront-5.1.6/app/views/spree/products/_cart_form.html.erb
|
|
13
|
+
# @gem-override devise-4.9.4/app/controllers/devise/sessions_controller.rb#create
|
|
14
|
+
#
|
|
15
|
+
# メソッド名 (`#method`) は省略可能で、省略時は対象ファイル全体を比較する
|
|
16
|
+
# (view / partial の丸ごと上書きを想定)。
|
|
17
|
+
class Marker
|
|
18
|
+
# マーカーは「コメント記号 + 半角スペース1個ちょうど + @gem-override」で始まる
|
|
19
|
+
# 行だけを有効とする。これにより、ドキュメント中で字下げして例示したもの
|
|
20
|
+
# (`# @gem-override ...` のようにスペースが2個以上) や、文中にバッククォート等で
|
|
21
|
+
# 引用したものを本物のマーカーと誤検出しない。
|
|
22
|
+
#
|
|
23
|
+
# 対応するコメント記号: Ruby (#) / ERB (<%#) / Slim・Haml (/)。
|
|
24
|
+
#
|
|
25
|
+
# gem 名はバージョンの直前のハイフンで区切る。gem 名側にハイフンや数字が
|
|
26
|
+
# 含まれていてもバージョン部分を正しく切り出せるよう、gem 名は最短一致とし
|
|
27
|
+
# 「数字始まりのトークン」をバージョンの開始位置とみなす。
|
|
28
|
+
# 各部の意味 (上から順に):
|
|
29
|
+
# \A[ \t]* 行頭の字下げ。メソッドの階層に応じた任意のインデント
|
|
30
|
+
# (?:\#|<%\#|\/) コメント記号。Ruby / ERB / Slim・Haml
|
|
31
|
+
# [ ] 半角スペース1個ちょうど。これで字下げ例を弾く
|
|
32
|
+
# @gem-override[ ] マーカー本体と区切りスペース1個
|
|
33
|
+
# gem gem 名。最短一致でバージョン直前まで
|
|
34
|
+
# version バージョン。数字始まり
|
|
35
|
+
# path gem 内の相対パス
|
|
36
|
+
# method メソッド名。省略可
|
|
37
|
+
PATTERN = /
|
|
38
|
+
\A[ \t]*
|
|
39
|
+
(?:\#|<%\#|\/)
|
|
40
|
+
[ ]
|
|
41
|
+
@gem-override[ ]
|
|
42
|
+
(?<gem>[a-z0-9_]+?)-
|
|
43
|
+
(?<version>\d[\w.\-]*)
|
|
44
|
+
\/
|
|
45
|
+
(?<path>[^\s\#]+)
|
|
46
|
+
(?:\#(?<method>\w+[!?=]?))?
|
|
47
|
+
(?:[ ]|%>|$)
|
|
48
|
+
/x
|
|
49
|
+
|
|
50
|
+
attr_reader :gem_name, :version, :path, :method_name, :file_path, :line_number
|
|
51
|
+
|
|
52
|
+
# マーカーを含む1行を解析する。マーカーでなければ nil を返す。
|
|
53
|
+
# file_path / line_number はマーカーが書かれた場所 (リポジトリ側) を記録する。
|
|
54
|
+
def self.parse(line, file_path: nil, line_number: nil)
|
|
55
|
+
match = PATTERN.match(line)
|
|
56
|
+
return nil unless match
|
|
57
|
+
|
|
58
|
+
new(
|
|
59
|
+
gem_name: match[:gem],
|
|
60
|
+
version: match[:version],
|
|
61
|
+
path: match[:path],
|
|
62
|
+
method_name: match[:method],
|
|
63
|
+
file_path: file_path,
|
|
64
|
+
line_number: line_number
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(gem_name:, version:, path:, method_name: nil, file_path: nil, line_number: nil)
|
|
69
|
+
@gem_name = gem_name
|
|
70
|
+
@version = version
|
|
71
|
+
@path = path
|
|
72
|
+
@method_name = method_name
|
|
73
|
+
@file_path = file_path
|
|
74
|
+
@line_number = line_number
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# メソッド名を含まない = ファイル全体を対象とするマーカーか。
|
|
78
|
+
def whole_file?
|
|
79
|
+
method_name.nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# マーカーが指す本家論理名 (狙い撃ち指定にそのまま使える1行表現)。
|
|
83
|
+
def logical_name
|
|
84
|
+
base = "#{gem_name}-#{version}/#{path}"
|
|
85
|
+
whole_file? ? base : "#{base}##{method_name}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "rails/railtie"
|
|
2
|
+
|
|
3
|
+
module GemOverrideMarker
|
|
4
|
+
# Rails アプリに組み込まれたとき、rake task を自動でロードする。
|
|
5
|
+
# gem 本体は Rails 非依存だが、利用側が Rails の場合の利便性のために用意する。
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
rake_tasks do
|
|
8
|
+
load File.expand_path("../tasks/gem_override_marker.rake", __dir__)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module GemOverrideMarker
|
|
2
|
+
# リポジトリ内に埋め込まれた `@gem-override` マーカーを収集し、
|
|
3
|
+
# 狙い撃ち指定 (ファイル / ファイル+メソッド / 本家論理名) で絞り込む。
|
|
4
|
+
class Registry
|
|
5
|
+
# マーカーを探索する対象ファイルの拡張子。Ruby と各種テンプレート。
|
|
6
|
+
SCANNABLE_EXTENSIONS = %w[.rb .erb .slim .haml].freeze
|
|
7
|
+
|
|
8
|
+
# 探索から除外するパス片 (相対パスの任意位置に含まれていれば除外)。
|
|
9
|
+
# tmp や依存ライブラリのほか、テスト用のダミーマーカーを誤検出しないよう
|
|
10
|
+
# spec/fixtures も除外する。
|
|
11
|
+
EXCLUDED_PATH_SEGMENTS = %w[
|
|
12
|
+
tmp/
|
|
13
|
+
node_modules/
|
|
14
|
+
vendor/bundle/
|
|
15
|
+
.git/
|
|
16
|
+
spec/fixtures/
|
|
17
|
+
test/fixtures/
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :root
|
|
21
|
+
|
|
22
|
+
def initialize(root: ".")
|
|
23
|
+
@root = root
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# リポジトリ全体を走査して全マーカーを返す。
|
|
27
|
+
def all_markers
|
|
28
|
+
target_files.flat_map { |file| markers_in_file(file) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 1ファイル内の全マーカーを返す。
|
|
32
|
+
def markers_in_file(file)
|
|
33
|
+
File.foreach(file).with_index(1).filter_map do |line, lineno|
|
|
34
|
+
Marker.parse(line, file_path: file, line_number: lineno)
|
|
35
|
+
end
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
# バイナリ等で読み取れない行があるファイルはスキップする。
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 狙い撃ち指定文字列からマーカー群を解決する。
|
|
42
|
+
# selector の解釈:
|
|
43
|
+
# - 既存ファイルパス → ファイル内の全マーカー
|
|
44
|
+
# - "ファイルパス,メソッド名" → そのメソッドのマーカーのみ
|
|
45
|
+
# - 本家論理名 (gem-版/パス#method) → 全マーカーから論理名一致を抽出
|
|
46
|
+
# - nil / 空 → リポジトリ全体の全マーカー
|
|
47
|
+
def resolve(selector)
|
|
48
|
+
return all_markers if selector.nil? || selector.strip.empty?
|
|
49
|
+
|
|
50
|
+
file_part, method_part = selector.split(",", 2).map { |s| s&.strip }
|
|
51
|
+
|
|
52
|
+
if File.file?(file_part)
|
|
53
|
+
markers = markers_in_file(file_part)
|
|
54
|
+
markers = markers.select { |m| m.method_name == method_part } if method_part
|
|
55
|
+
return markers
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ファイルとして存在しなければ本家論理名指定とみなす。
|
|
59
|
+
all_markers.select { |m| m.logical_name == selector.strip }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# 走査対象のファイル一覧 (除外パスと対象外拡張子を除く)。
|
|
65
|
+
def target_files
|
|
66
|
+
pattern = File.join(root, "**", "*")
|
|
67
|
+
Dir.glob(pattern, File::FNM_DOTMATCH).select do |path|
|
|
68
|
+
next false unless File.file?(path)
|
|
69
|
+
next false unless SCANNABLE_EXTENSIONS.include?(File.extname(path))
|
|
70
|
+
|
|
71
|
+
relative = path.delete_prefix("#{root}/")
|
|
72
|
+
EXCLUDED_PATH_SEGMENTS.none? { |segment| relative.include?(segment) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "prism"
|
|
2
|
+
|
|
3
|
+
module GemOverrideMarker
|
|
4
|
+
# Ruby ソースから対象メソッドの実装を抽出し、インデントを正規化する。
|
|
5
|
+
#
|
|
6
|
+
# `def` から `end` までを正規表現で切り出すことはネストした `end` の判別が
|
|
7
|
+
# できず不可能なため、Ruby 3.3 標準の prism で AST 解析してメソッド単位で
|
|
8
|
+
# slice する。本家ソース・override の双方を同じ方式で抽出することで、
|
|
9
|
+
# クラス直下 (本家) と module 多階層 (decorator) のネスト差を吸収できる。
|
|
10
|
+
class SourceExtractor
|
|
11
|
+
class MethodNotFoundError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# 指定ファイルから対象メソッドの実装を抽出して dedent する。
|
|
14
|
+
# method_name が nil の場合はファイル全体を返す (view / partial 等)。
|
|
15
|
+
def self.extract(file_path, method_name)
|
|
16
|
+
source = File.read(file_path)
|
|
17
|
+
return dedent(source) if method_name.nil?
|
|
18
|
+
|
|
19
|
+
sliced = slice_method(source, method_name.to_sym)
|
|
20
|
+
raise MethodNotFoundError, "method #{method_name} not found in #{file_path}" if sliced.nil?
|
|
21
|
+
|
|
22
|
+
dedent(sliced)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# AST を走査して対象メソッドの DefNode を探し、その範囲を slice する。
|
|
26
|
+
# 最初に見つかった定義を返す。見つからなければ nil。
|
|
27
|
+
def self.slice_method(source, method_name)
|
|
28
|
+
result = Prism.parse(source)
|
|
29
|
+
found = nil
|
|
30
|
+
|
|
31
|
+
visit = lambda do |node|
|
|
32
|
+
return if node.nil? || found
|
|
33
|
+
|
|
34
|
+
if node.is_a?(Prism::DefNode) && node.name == method_name
|
|
35
|
+
found = node.location.slice
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
node.compact_child_nodes.each { |child| visit.call(child) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
visit.call(result.value)
|
|
43
|
+
found
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# prism の slice は 1行目 (def) が左寄せ・body 行が元のインデントのまま
|
|
47
|
+
# 返るため、body の最小インデントを def の位置まで詰めてネスト差を吸収する。
|
|
48
|
+
def self.dedent(code)
|
|
49
|
+
lines = code.lines
|
|
50
|
+
return code if lines.empty?
|
|
51
|
+
|
|
52
|
+
base_indent = indent_width(lines.first)
|
|
53
|
+
body = lines[1..] || []
|
|
54
|
+
body_indents = body.reject { |line| line.strip.empty? }.map { |line| indent_width(line) }
|
|
55
|
+
min_body_indent = body_indents.min || base_indent
|
|
56
|
+
|
|
57
|
+
remove = [ min_body_indent - base_indent, 0 ].max
|
|
58
|
+
|
|
59
|
+
result = [ lines.first ]
|
|
60
|
+
result += body.map { |line| line.strip.empty? ? line : line[remove..] }
|
|
61
|
+
result.join
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 行頭の半角スペース数を返す。
|
|
65
|
+
def self.indent_width(line)
|
|
66
|
+
line[/\A */].size
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require "gem_override_marker/version"
|
|
2
|
+
require "gem_override_marker/marker"
|
|
3
|
+
require "gem_override_marker/source_extractor"
|
|
4
|
+
require "gem_override_marker/gem_source"
|
|
5
|
+
require "gem_override_marker/diff_generator"
|
|
6
|
+
require "gem_override_marker/registry"
|
|
7
|
+
|
|
8
|
+
# Rails 上で使われる場合のみ rake task を自動ロードする (gem 本体は Rails 非依存)。
|
|
9
|
+
require "gem_override_marker/railtie" if defined?(Rails::Railtie)
|
|
10
|
+
|
|
11
|
+
# gem のメソッド prepend 再実装 / view 丸ごと上書きが、本家のどの版の何を
|
|
12
|
+
# 基準にしているかを `@gem-override` マーカーで管理し、本家との diff を
|
|
13
|
+
# 必要なときに生成する。gem アップグレード時の上書きの追随を支援するツール。
|
|
14
|
+
module GemOverrideMarker
|
|
15
|
+
# check の結果1件。マーカーの版と Gemfile.lock の版のズレを表す。
|
|
16
|
+
Mismatch = Struct.new(:marker, :locked_version, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# 狙い撃ち指定にマッチするマーカー一覧を返す (list 用)。
|
|
21
|
+
def list(selector: nil, root: ".")
|
|
22
|
+
Registry.new(root: root).resolve(selector)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 1マーカーについて本家ソース vs override の unified diff を生成する。
|
|
26
|
+
# 比較対象の本家はマーカーに書かれた版を使う。
|
|
27
|
+
def diff_for(marker)
|
|
28
|
+
source_file = GemSource.resolve_file(marker)
|
|
29
|
+
source_code = SourceExtractor.extract(source_file, marker.method_name)
|
|
30
|
+
override_code = SourceExtractor.extract(marker.file_path, marker.method_name)
|
|
31
|
+
|
|
32
|
+
method_suffix = marker.method_name ? "##{marker.method_name}" : ""
|
|
33
|
+
DiffGenerator.unified(
|
|
34
|
+
source_code, override_code,
|
|
35
|
+
old_label: "source #{marker.gem_name}-#{marker.version}/#{marker.path}#{method_suffix}",
|
|
36
|
+
new_label: "override #{marker.file_path}#{method_suffix}"
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 同一マーカー対象 (gem 内の同じパス・同じメソッド) について、本家の
|
|
41
|
+
# 2版間 (from_version → to_version) の unified diff を生成する。
|
|
42
|
+
#
|
|
43
|
+
# gem アップグレード時に「その上書き箇所が本家側で変わったか」を選別する
|
|
44
|
+
# ための道具。差分が空 = 本家に変更なし → 上書きコードは触らず版表記だけ
|
|
45
|
+
# 更新すればよい。差分あり = 本家が変わった → 自分の diff を新本家に貼り直す。
|
|
46
|
+
#
|
|
47
|
+
# to_version を省略すると Gemfile.lock 上の実バージョン (アップグレード後の版)
|
|
48
|
+
# を使う。本家同士の差分マージはしない設計思想に沿い、あくまで選別用途。
|
|
49
|
+
def upstream_diff(marker, from_version: nil, to_version: nil)
|
|
50
|
+
from = from_version || marker.version
|
|
51
|
+
to = to_version || GemSource.locked_version(marker.gem_name)
|
|
52
|
+
raise GemSource::SourceNotFoundError, "could not resolve to_version for #{marker.gem_name}" if to.nil?
|
|
53
|
+
|
|
54
|
+
from_file = GemSource.resolve_path(marker.gem_name, from, marker.path)
|
|
55
|
+
to_file = GemSource.resolve_path(marker.gem_name, to, marker.path)
|
|
56
|
+
|
|
57
|
+
from_code = SourceExtractor.extract(from_file, marker.method_name)
|
|
58
|
+
to_code = SourceExtractor.extract(to_file, marker.method_name)
|
|
59
|
+
|
|
60
|
+
method_suffix = marker.method_name ? "##{marker.method_name}" : ""
|
|
61
|
+
DiffGenerator.unified(
|
|
62
|
+
from_code, to_code,
|
|
63
|
+
old_label: "upstream #{marker.gem_name}-#{from}/#{marker.path}#{method_suffix}",
|
|
64
|
+
new_label: "upstream #{marker.gem_name}-#{to}/#{marker.path}#{method_suffix}"
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# マーカーの版と Gemfile.lock の版がズレているものを抽出する (check 用)。
|
|
69
|
+
# ズレが無ければ空配列。lockfile_path を省略すると Bundler が解決した
|
|
70
|
+
# プロジェクトの Gemfile.lock を使う。
|
|
71
|
+
def check(selector: nil, root: ".", lockfile_path: nil)
|
|
72
|
+
list(selector: selector, root: root).filter_map do |marker|
|
|
73
|
+
locked = GemSource.locked_version(marker.gem_name, lockfile_path)
|
|
74
|
+
next if locked.nil? || locked == marker.version
|
|
75
|
+
|
|
76
|
+
Mismatch.new(marker: marker, locked_version: locked)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "gem_override_marker"
|
|
2
|
+
|
|
3
|
+
# gem の override マーカーを管理する rake task 群。
|
|
4
|
+
#
|
|
5
|
+
# 本家ソースをテキストとして解析するだけなので `:environment` には依存しない。
|
|
6
|
+
# DB 接続やアプリ初期化を起動せずに動く。
|
|
7
|
+
namespace :gem_override_marker do
|
|
8
|
+
desc "List all @gem-override markers (optionally narrowed by selector)"
|
|
9
|
+
task :list, [ :selector ] do |_task, args|
|
|
10
|
+
markers = GemOverrideMarker.list(selector: args[:selector])
|
|
11
|
+
|
|
12
|
+
if markers.empty?
|
|
13
|
+
puts "No markers found."
|
|
14
|
+
next
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
markers.each do |marker|
|
|
18
|
+
puts "#{marker.file_path}:#{marker.line_number}\t#{marker.logical_name}"
|
|
19
|
+
end
|
|
20
|
+
puts "\n#{markers.size} marker(s)"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Generate diff between gem source and override (arg: selector)"
|
|
24
|
+
task :diff, [ :selector ] do |_task, args|
|
|
25
|
+
markers = GemOverrideMarker.list(selector: args[:selector])
|
|
26
|
+
|
|
27
|
+
if markers.empty?
|
|
28
|
+
puts "No matching markers found: #{args[:selector]}"
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
markers.each do |marker|
|
|
33
|
+
puts "=" * 72
|
|
34
|
+
puts "#{marker.file_path}:#{marker.line_number} #{marker.logical_name}"
|
|
35
|
+
puts "=" * 72
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
diff = GemOverrideMarker.diff_for(marker)
|
|
39
|
+
puts diff.empty? ? "(no difference)" : diff
|
|
40
|
+
rescue GemOverrideMarker::GemSource::SourceNotFoundError,
|
|
41
|
+
GemOverrideMarker::SourceExtractor::MethodNotFoundError => e
|
|
42
|
+
warn "⚠️ #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc "Diff upstream between two versions for each marker (args: selector, to_version)"
|
|
48
|
+
task :upstream_diff, [ :selector, :to_version ] do |_task, args|
|
|
49
|
+
markers = GemOverrideMarker.list(selector: args[:selector])
|
|
50
|
+
|
|
51
|
+
if markers.empty?
|
|
52
|
+
puts "No matching markers found: #{args[:selector]}"
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
markers.each do |marker|
|
|
57
|
+
puts "=" * 72
|
|
58
|
+
puts "#{marker.file_path}:#{marker.line_number} #{marker.logical_name}"
|
|
59
|
+
puts "=" * 72
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
diff = GemOverrideMarker.upstream_diff(marker, to_version: args[:to_version])
|
|
63
|
+
puts diff.empty? ? "(no upstream change)" : diff
|
|
64
|
+
rescue GemOverrideMarker::GemSource::SourceNotFoundError,
|
|
65
|
+
GemOverrideMarker::SourceExtractor::MethodNotFoundError => e
|
|
66
|
+
warn "⚠️ #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "Detect markers whose version differs from Gemfile.lock (exits 1 on mismatch)"
|
|
72
|
+
task :check, [ :selector ] do |_task, args|
|
|
73
|
+
mismatches = GemOverrideMarker.check(selector: args[:selector])
|
|
74
|
+
|
|
75
|
+
if mismatches.empty?
|
|
76
|
+
puts "✅ All marker versions match Gemfile.lock."
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
warn "❌ Marker versions differ from Gemfile.lock (possible outdated overrides):"
|
|
81
|
+
mismatches.each do |mismatch|
|
|
82
|
+
marker = mismatch.marker
|
|
83
|
+
warn " #{marker.file_path}:#{marker.line_number}"
|
|
84
|
+
warn " marker version: #{marker.gem_name}-#{marker.version}"
|
|
85
|
+
warn " locked version: #{marker.gem_name}-#{mismatch.locked_version}"
|
|
86
|
+
end
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
end
|