gemlens 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/.github/workflows/main.yml +18 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +10 -0
- data/1git +6 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/gemlens +6 -0
- data/bin/setup +8 -0
- data/gemlens.gemspec +29 -0
- data/lib/gemlens/gems.yml +16392 -0
- data/lib/gemlens/history.rb +185 -0
- data/lib/gemlens/version.rb +5 -0
- data/lib/gemlens.rb +9 -0
- metadata +83 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
require "date"
|
2
|
+
require "json"
|
3
|
+
require "open3"
|
4
|
+
require "colorize"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
module Gemlens
|
8
|
+
GEMS_YML_PATH = File.expand_path("../../../data/gems.yml", __FILE__)
|
9
|
+
GEMS_YML = File.exist?(GEMS_YML_PATH) ? YAML.load_file(GEMS_YML_PATH) : []
|
10
|
+
|
11
|
+
GEM_TO_REPOS = GEMS_YML.each_with_object({}) do |entry, hash|
|
12
|
+
hash[entry["full_name"]] = entry["repos"] || []
|
13
|
+
end
|
14
|
+
|
15
|
+
class GemfileHistoryAnalyzer
|
16
|
+
def initialize(repo_path = ".")
|
17
|
+
@repo_path = repo_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def gemfile_commits
|
21
|
+
Dir.chdir(@repo_path) do
|
22
|
+
unless system("git rev-parse HEAD > /dev/null 2>&1")
|
23
|
+
puts "⚠️ No commits found in the repository yet.".colorize(:yellow)
|
24
|
+
return []
|
25
|
+
end
|
26
|
+
|
27
|
+
`git log --follow --format="%H|%an|%ad|%s" --date=iso Gemfile`.lines.map do |line|
|
28
|
+
sha, author, date, message = line.strip.split("|", 4)
|
29
|
+
|
30
|
+
# Extract PR number if present
|
31
|
+
pr = message[/(?:#|PR\s*)(\d+)/i, 1]
|
32
|
+
|
33
|
+
{
|
34
|
+
sha: sha,
|
35
|
+
author: author,
|
36
|
+
date: DateTime.parse(date),
|
37
|
+
message: message.strip,
|
38
|
+
pr: pr
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_gemfile_at(commit_sha)
|
45
|
+
content = Dir.chdir(@repo_path) do
|
46
|
+
`git show #{commit_sha}:Gemfile`
|
47
|
+
end
|
48
|
+
|
49
|
+
gems = {}
|
50
|
+
content.each_line do |line|
|
51
|
+
if line =~ /^\s*gem\s+["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?/
|
52
|
+
name = $1
|
53
|
+
version = $2
|
54
|
+
gems[name] = version
|
55
|
+
end
|
56
|
+
end
|
57
|
+
gems
|
58
|
+
end
|
59
|
+
|
60
|
+
def analyze
|
61
|
+
history = Hash.new { |h, k| h[k] = [] }
|
62
|
+
|
63
|
+
if gemfile_commits.size < 2
|
64
|
+
puts "📘 Only one commit related to Gemfile found. Not enough history to analyze changes.".colorize(:light_blue)
|
65
|
+
return history
|
66
|
+
end
|
67
|
+
|
68
|
+
gemfile_commits.reverse.each_cons(2) do |older, newer|
|
69
|
+
before = parse_gemfile_at(older[:sha])
|
70
|
+
after = parse_gemfile_at(newer[:sha])
|
71
|
+
|
72
|
+
added = after.keys - before.keys
|
73
|
+
removed = before.keys - after.keys
|
74
|
+
common = before.keys & after.keys
|
75
|
+
|
76
|
+
added.each do |gem_name|
|
77
|
+
history[gem_name] << build_event("added", gem_name, newer.merge(version: after[gem_name]))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Only gems that exist in both and changed version count as updated
|
81
|
+
updated = common.select do |gem_name|
|
82
|
+
before[gem_name] != after[gem_name]
|
83
|
+
end
|
84
|
+
|
85
|
+
removed.each do |gem_name|
|
86
|
+
history[gem_name] << build_event("removed", gem_name, newer.merge(
|
87
|
+
version: before[gem_name]
|
88
|
+
))
|
89
|
+
end
|
90
|
+
|
91
|
+
updated -= removed
|
92
|
+
|
93
|
+
updated.each do |gem_name|
|
94
|
+
history[gem_name] << build_event("updated", gem_name, newer.merge(
|
95
|
+
version_from: before[gem_name],
|
96
|
+
version_to: after[gem_name]
|
97
|
+
))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
history
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_event(action, gem_name, commit)
|
105
|
+
{
|
106
|
+
action: action,
|
107
|
+
by: commit[:author],
|
108
|
+
at: commit[:date].to_s,
|
109
|
+
message: commit[:message],
|
110
|
+
pr: commit[:pr],
|
111
|
+
sha: commit[:sha],
|
112
|
+
version: commit[:version],
|
113
|
+
version_from: commit[:version_from], # only present on update
|
114
|
+
version_to: commit[:version_to] # only present on update
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.color_tag(action)
|
120
|
+
case action
|
121
|
+
when "added" then "🟩".colorize(:green)
|
122
|
+
when "removed" then "🟥".colorize(:red)
|
123
|
+
when "updated" then "🟦".colorize(:blue)
|
124
|
+
else "⬜".colorize(:light_black)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.print_timeline(history)
|
129
|
+
puts "\n📜 Gemfile History Timeline\n\n"
|
130
|
+
flattened = history.flat_map do |gem, events|
|
131
|
+
events.map do |e|
|
132
|
+
{
|
133
|
+
date: DateTime.parse(e[:at]),
|
134
|
+
at: e[:at],
|
135
|
+
action: e[:action],
|
136
|
+
gem: gem,
|
137
|
+
author: e[:by],
|
138
|
+
message: e[:message],
|
139
|
+
pr: e[:pr],
|
140
|
+
sha: e[:sha],
|
141
|
+
version: e[:version],
|
142
|
+
version_from: e[:version_from],
|
143
|
+
version_to: e[:version_to],
|
144
|
+
repo: e[:repo]
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end.sort_by { |e| e[:date] }
|
148
|
+
|
149
|
+
flattened.each do |e|
|
150
|
+
label = e[:action].capitalize.ljust(8)
|
151
|
+
|
152
|
+
version_str =
|
153
|
+
if e[:action] == "updated" && e[:version_from] && e[:version_to]
|
154
|
+
" (#{e[:version_from]} → #{e[:version_to]})"
|
155
|
+
elsif e[:version]
|
156
|
+
" (#{e[:version]})"
|
157
|
+
else
|
158
|
+
""
|
159
|
+
end
|
160
|
+
|
161
|
+
gem_label = case e[:action]
|
162
|
+
when "added" then (e[:gem] + version_str).ljust(20).colorize(:green)
|
163
|
+
when "removed" then (e[:gem] + version_str).ljust(20).colorize(:red)
|
164
|
+
when "updated" then (e[:gem] + version_str).ljust(20).colorize(:blue)
|
165
|
+
else (e[:gem] + version_str).ljust(20)
|
166
|
+
end
|
167
|
+
|
168
|
+
pr_info = if e[:pr]
|
169
|
+
"PR ##{e[:pr].rjust(4)}"
|
170
|
+
elsif e[:sha]
|
171
|
+
e[:sha][0..6]
|
172
|
+
else
|
173
|
+
"—"
|
174
|
+
end
|
175
|
+
|
176
|
+
puts "#{color_tag(e[:action])} #{e[:date].strftime('%Y-%m-%d')} " \
|
177
|
+
"#{label} #{gem_label} by #{e[:author].ljust(10)} ➜ #{pr_info} | #{e[:message]}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.run(repo_path = ".")
|
182
|
+
history = GemfileHistoryAnalyzer.new(repo_path).analyze
|
183
|
+
print_timeline(history) unless history.empty?
|
184
|
+
end
|
185
|
+
end
|
data/lib/gemlens.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gemlens
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fai Wong
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.0
|
27
|
+
description: GemLens is a developer tool that analyzes the history of your Gemfile
|
28
|
+
using Git and presents a timeline of gem additions, removals, and version updates.
|
29
|
+
It's useful for auditing dependency changes, generating changelogs, and understanding
|
30
|
+
how your project's gem dependencies evolved. Built for maintainers, teams, and curious
|
31
|
+
developers.
|
32
|
+
email:
|
33
|
+
- wongwf82@gmail.com
|
34
|
+
executables:
|
35
|
+
- console
|
36
|
+
- gemlens
|
37
|
+
- setup
|
38
|
+
extensions: []
|
39
|
+
extra_rdoc_files: []
|
40
|
+
files:
|
41
|
+
- ".github/workflows/main.yml"
|
42
|
+
- ".gitignore"
|
43
|
+
- ".rubocop.yml"
|
44
|
+
- 1git
|
45
|
+
- CODE_OF_CONDUCT.md
|
46
|
+
- Gemfile
|
47
|
+
- Gemfile.lock
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- bin/console
|
52
|
+
- bin/gemlens
|
53
|
+
- bin/setup
|
54
|
+
- gemlens.gemspec
|
55
|
+
- lib/gemlens.rb
|
56
|
+
- lib/gemlens/gems.yml
|
57
|
+
- lib/gemlens/history.rb
|
58
|
+
- lib/gemlens/version.rb
|
59
|
+
homepage: https://github.com/BestBitsLab/gemlens
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata:
|
63
|
+
source_code_uri: https://github.com/BestBitsLab/gemlens
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.3.0
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubygems_version: 3.2.3
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Track and visualize changes to your Gemfile over time.
|
83
|
+
test_files: []
|