gemlens 0.1.0 ā 0.1.1
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 +4 -4
- data/.rubocop.yml +33 -3
- data/README.md +2 -0
- data/bin/gemlens +1 -0
- data/gemlens.gemspec +13 -10
- data/lib/gemlens/history.rb +152 -94
- data/lib/gemlens/version.rb +1 -1
- metadata +3 -5
- data/1git +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6106a3dd7bed953c122eab3044de5e900f07db400c1b3791d4140a3f04ff711
|
4
|
+
data.tar.gz: f62f3c32cc9c7e5da07bf9748aa98e50041a0414549027ceda1d1afaef0b3294
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20a1273a451de9ecfc6afce47ee3cd8e58c92f066bc0d40406b4f6d44285ed7d24019b37595f60aa0932bf9e0c043aac2692c064f697908857f27676c62cf0b4
|
7
|
+
data.tar.gz: 8d73560c244c6dabd76341148a7586783e7b73900615ce7e95a1fc0ff6ce1974001ea60140595b563a74140ea2261b875ca50454e4914f22df7647f5aed3b124
|
data/.rubocop.yml
CHANGED
@@ -1,3 +1,36 @@
|
|
1
|
+
Layout/LineLength:
|
2
|
+
Max: 120
|
3
|
+
|
4
|
+
Metrics/AbcSize:
|
5
|
+
Enabled: true
|
6
|
+
Max: 30
|
7
|
+
Details: >
|
8
|
+
Code that involves a lot of branches can be very hard to wrap your head
|
9
|
+
around.
|
10
|
+
|
11
|
+
Metrics/ClassLength:
|
12
|
+
Max: 110
|
13
|
+
|
14
|
+
Metrics/MethodLength:
|
15
|
+
Enabled: true
|
16
|
+
Max: 30
|
17
|
+
Details: >
|
18
|
+
Long methods can be very hard to review. Consider splitting this method up
|
19
|
+
into separate methods.
|
20
|
+
|
21
|
+
Metrics/ParameterLists:
|
22
|
+
Max: 6
|
23
|
+
|
24
|
+
Metrics/PerceivedComplexity:
|
25
|
+
Enabled: true
|
26
|
+
Max: 14
|
27
|
+
Description: >
|
28
|
+
A complexity metric geared towards measuring complexity for a
|
29
|
+
human reader.
|
30
|
+
|
31
|
+
Style/Documentation:
|
32
|
+
Enabled: false
|
33
|
+
|
1
34
|
Style/StringLiterals:
|
2
35
|
Enabled: true
|
3
36
|
EnforcedStyle: double_quotes
|
@@ -5,6 +38,3 @@ Style/StringLiterals:
|
|
5
38
|
Style/StringLiteralsInInterpolation:
|
6
39
|
Enabled: true
|
7
40
|
EnforcedStyle: double_quotes
|
8
|
-
|
9
|
-
Layout/LineLength:
|
10
|
-
Max: 120
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# GemLens
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/gemlens)
|
4
|
+
|
3
5
|
**Gemlens** is a CLI tool to analyze the evolution of your `Gemfile` over time. It helps you track when gems were added, removed, or updated in a project ā great for audits, debugging, or historical exploration.
|
4
6
|
|
5
7
|
---
|
data/bin/gemlens
CHANGED
data/gemlens.gemspec
CHANGED
@@ -3,15 +3,18 @@
|
|
3
3
|
require_relative "lib/gemlens/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name
|
7
|
-
s.version
|
8
|
-
s.authors
|
9
|
-
s.email
|
6
|
+
s.name = "gemlens"
|
7
|
+
s.version = Gemlens::VERSION
|
8
|
+
s.authors = ["Fai Wong"]
|
9
|
+
s.email = ["wongwf82@gmail.com"]
|
10
10
|
|
11
|
-
s.summary
|
12
|
-
s.description = "GemLens is a developer tool that analyzes the history of your Gemfile using Git
|
13
|
-
|
14
|
-
|
11
|
+
s.summary = "Track and visualize changes to your Gemfile over time."
|
12
|
+
s.description = "GemLens is a developer tool that analyzes the history of your Gemfile using Git " \
|
13
|
+
"and presents a timeline of gem additions, removals, and version updates. It's useful " \
|
14
|
+
"for auditing dependency changes, generating changelogs, and understanding how your " \
|
15
|
+
"project's gem dependencies evolved."
|
16
|
+
s.license = "MIT"
|
17
|
+
s.required_ruby_version = ">= 2.4.0"
|
15
18
|
|
16
19
|
s.homepage = "https://github.com/BestBitsLab/gemlens"
|
17
20
|
s.metadata["source_code_uri"] = "https://github.com/BestBitsLab/gemlens"
|
@@ -21,8 +24,8 @@ Gem::Specification.new do |s|
|
|
21
24
|
s.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
25
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
23
26
|
end
|
24
|
-
s.bindir
|
25
|
-
s.executables
|
27
|
+
s.bindir = "bin"
|
28
|
+
s.executables = s.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
26
29
|
s.require_paths = ["lib"]
|
27
30
|
|
28
31
|
s.add_dependency "colorize", "~> 1.1.0"
|
data/lib/gemlens/history.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "date"
|
2
4
|
require "json"
|
3
5
|
require "open3"
|
@@ -5,7 +7,7 @@ require "colorize"
|
|
5
7
|
require "yaml"
|
6
8
|
|
7
9
|
module Gemlens
|
8
|
-
GEMS_YML_PATH = File.expand_path("
|
10
|
+
GEMS_YML_PATH = File.expand_path("../../data/gems.yml", __dir__)
|
9
11
|
GEMS_YML = File.exist?(GEMS_YML_PATH) ? YAML.load_file(GEMS_YML_PATH) : []
|
10
12
|
|
11
13
|
GEM_TO_REPOS = GEMS_YML.each_with_object({}) do |entry, hash|
|
@@ -17,91 +19,119 @@ module Gemlens
|
|
17
19
|
@repo_path = repo_path
|
18
20
|
end
|
19
21
|
|
20
|
-
def
|
21
|
-
|
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
|
22
|
+
def analyze
|
23
|
+
return not_enough_history if gemfile_commits.size < 2
|
26
24
|
|
27
|
-
|
28
|
-
sha, author, date, message = line.strip.split("|", 4)
|
25
|
+
history = Hash.new { |h, k| h[k] = [] }
|
29
26
|
|
30
|
-
|
31
|
-
|
27
|
+
gemfile_commits.reverse.each_cons(2) do |older, newer|
|
28
|
+
before = parse_gemfile_at(older[:sha])
|
29
|
+
after = parse_gemfile_at(newer[:sha])
|
32
30
|
|
33
|
-
|
34
|
-
sha: sha,
|
35
|
-
author: author,
|
36
|
-
date: DateTime.parse(date),
|
37
|
-
message: message.strip,
|
38
|
-
pr: pr
|
39
|
-
}
|
40
|
-
end
|
31
|
+
process_changes(before, after, newer, history)
|
41
32
|
end
|
33
|
+
|
34
|
+
history
|
42
35
|
end
|
43
36
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
37
|
+
def not_enough_history
|
38
|
+
puts "š Only one commit related to Gemfile found. Not enough history to analyze changes.".colorize(:light_blue)
|
39
|
+
{}
|
40
|
+
end
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
42
|
+
def process_changes(before, after, commit, history)
|
43
|
+
added = after.keys - before.keys
|
44
|
+
removed = before.keys - after.keys
|
45
|
+
common = before.keys & after.keys
|
46
|
+
|
47
|
+
handle_added(added, after, commit, history)
|
48
|
+
handle_removed(removed, before, commit, history)
|
49
|
+
handle_updated(common, before, after, removed, commit, history)
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_added(added, after, commit, history)
|
53
|
+
added.each do |gem_name|
|
54
|
+
history[gem_name] << build_event(
|
55
|
+
"added", gem_name,
|
56
|
+
commit.merge(version: after[gem_name])
|
57
|
+
)
|
56
58
|
end
|
57
|
-
gems
|
58
59
|
end
|
59
60
|
|
60
|
-
def
|
61
|
-
|
61
|
+
def handle_removed(removed, before, commit, history)
|
62
|
+
removed.each do |gem_name|
|
63
|
+
history[gem_name] << build_event(
|
64
|
+
"removed", gem_name,
|
65
|
+
commit.merge(version: before[gem_name])
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
62
69
|
|
63
|
-
|
64
|
-
|
65
|
-
|
70
|
+
def handle_updated(common, before, after, removed, commit, history)
|
71
|
+
updated = common.select do |gem_name|
|
72
|
+
before[gem_name] != after[gem_name] && !removed.include?(gem_name)
|
66
73
|
end
|
67
74
|
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
updated.each do |gem_name|
|
76
|
+
history[gem_name] << build_event(
|
77
|
+
"updated", gem_name,
|
78
|
+
commit.merge(
|
79
|
+
version_from: before[gem_name],
|
80
|
+
version_to: after[gem_name]
|
81
|
+
)
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
71
85
|
|
72
|
-
|
73
|
-
|
74
|
-
common = before.keys & after.keys
|
86
|
+
def gemfile_commits
|
87
|
+
return [] unless valid_git_repo?
|
75
88
|
|
76
|
-
|
77
|
-
|
78
|
-
|
89
|
+
Dir.chdir(@repo_path) do
|
90
|
+
parse_git_log(`git log --follow --format="%H|%an|%ad|%s" --date=iso Gemfile`)
|
91
|
+
end
|
92
|
+
end
|
79
93
|
|
80
|
-
|
81
|
-
|
82
|
-
|
94
|
+
def valid_git_repo?
|
95
|
+
Dir.chdir(@repo_path) do
|
96
|
+
system("git rev-parse HEAD > /dev/null 2>&1") || begin
|
97
|
+
puts "ā ļø No commits found in the repository yet.".colorize(:yellow)
|
98
|
+
false
|
83
99
|
end
|
100
|
+
end
|
101
|
+
end
|
84
102
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
103
|
+
def parse_git_log(log_output)
|
104
|
+
log_output.lines.map do |line|
|
105
|
+
sha, author, date, message = line.strip.split("|", 4)
|
106
|
+
pr = message[/(?:#|PR\s*)(\d+)/i, 1]
|
90
107
|
|
91
|
-
|
108
|
+
{
|
109
|
+
sha: sha,
|
110
|
+
author: author,
|
111
|
+
date: DateTime.parse(date),
|
112
|
+
message: message.strip,
|
113
|
+
pr: pr
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
92
117
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
version_to: after[gem_name]
|
97
|
-
))
|
98
|
-
end
|
118
|
+
def parse_gemfile_at(commit_sha)
|
119
|
+
content = Dir.chdir(@repo_path) do
|
120
|
+
`git show #{commit_sha}:Gemfile`
|
99
121
|
end
|
100
122
|
|
101
|
-
|
123
|
+
gems = {}
|
124
|
+
content.each_line do |line|
|
125
|
+
next unless (match = line.match(/^\s*gem\s+["'](?<name>[^"']+)["'](?:\s*,\s*["'](?<version>[^"']+)["'])?/))
|
126
|
+
|
127
|
+
name = match[:name]
|
128
|
+
version = match[:version]
|
129
|
+
gems[name] = version
|
130
|
+
end
|
131
|
+
gems
|
102
132
|
end
|
103
133
|
|
104
|
-
def build_event(action,
|
134
|
+
def build_event(action, _gem_name, commit)
|
105
135
|
{
|
106
136
|
action: action,
|
107
137
|
by: commit[:author],
|
@@ -125,8 +155,7 @@ module Gemlens
|
|
125
155
|
end
|
126
156
|
end
|
127
157
|
|
128
|
-
def self.
|
129
|
-
puts "\nš Gemfile History Timeline\n\n"
|
158
|
+
def self.flatten_and_sort(history)
|
130
159
|
flattened = history.flat_map do |gem, events|
|
131
160
|
events.map do |e|
|
132
161
|
{
|
@@ -144,37 +173,66 @@ module Gemlens
|
|
144
173
|
repo: e[:repo]
|
145
174
|
}
|
146
175
|
end
|
147
|
-
end
|
148
|
-
|
149
|
-
flattened.
|
150
|
-
|
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
|
176
|
+
end
|
177
|
+
|
178
|
+
flattened.sort_by { |e| e[:date] }
|
179
|
+
end
|
160
180
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
181
|
+
def self.format_event_line(event)
|
182
|
+
[
|
183
|
+
color_tag(event[:action]),
|
184
|
+
event[:date].strftime("%Y-%m-%d"),
|
185
|
+
format_label(event),
|
186
|
+
format_gem(event),
|
187
|
+
"by #{event[:author].ljust(10)}",
|
188
|
+
"ā #{format_pr(event)}",
|
189
|
+
"| #{event[:message]}"
|
190
|
+
].join(" ").strip
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.format_label(event)
|
194
|
+
event[:action].capitalize.ljust(8)
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.format_gem(event)
|
198
|
+
version_str = format_version(event)
|
199
|
+
text = (event[:gem] + version_str).ljust(20)
|
200
|
+
colorize_by_action(text, event[:action])
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.format_version(event)
|
204
|
+
if event[:action] == "updated" && event[:version_from] && event[:version_to]
|
205
|
+
" (#{event[:version_from]} ā #{event[:version_to]})"
|
206
|
+
elsif event[:version]
|
207
|
+
" (#{event[:version]})"
|
208
|
+
else
|
209
|
+
""
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.colorize_by_action(text, action)
|
214
|
+
case action
|
215
|
+
when "added" then text.colorize(:green)
|
216
|
+
when "removed" then text.colorize(:red)
|
217
|
+
when "updated" then text.colorize(:blue)
|
218
|
+
else text
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.format_pr(event)
|
223
|
+
if event[:pr]
|
224
|
+
"PR ##{event[:pr].rjust(4)}"
|
225
|
+
elsif event[:sha]
|
226
|
+
event[:sha][0..6]
|
227
|
+
else
|
228
|
+
"ā"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.print_timeline(history)
|
233
|
+
puts "\nš Gemfile History Timeline\n\n"
|
234
|
+
flatten_and_sort(history).each do |e|
|
235
|
+
puts format_event_line(e)
|
178
236
|
end
|
179
237
|
end
|
180
238
|
|
data/lib/gemlens/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gemlens
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fai Wong
|
@@ -27,8 +27,7 @@ dependencies:
|
|
27
27
|
description: GemLens is a developer tool that analyzes the history of your Gemfile
|
28
28
|
using Git and presents a timeline of gem additions, removals, and version updates.
|
29
29
|
It's useful for auditing dependency changes, generating changelogs, and understanding
|
30
|
-
how your project's gem dependencies evolved.
|
31
|
-
developers.
|
30
|
+
how your project's gem dependencies evolved.
|
32
31
|
email:
|
33
32
|
- wongwf82@gmail.com
|
34
33
|
executables:
|
@@ -41,7 +40,6 @@ files:
|
|
41
40
|
- ".github/workflows/main.yml"
|
42
41
|
- ".gitignore"
|
43
42
|
- ".rubocop.yml"
|
44
|
-
- 1git
|
45
43
|
- CODE_OF_CONDUCT.md
|
46
44
|
- Gemfile
|
47
45
|
- Gemfile.lock
|
@@ -69,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
67
|
requirements:
|
70
68
|
- - ">="
|
71
69
|
- !ruby/object:Gem::Version
|
72
|
-
version: 2.
|
70
|
+
version: 2.4.0
|
73
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
72
|
requirements:
|
75
73
|
- - ">="
|
data/1git
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
fatal: ambiguous argument 'log': unknown revision or path not in the working tree.
|
2
|
-
Use '--' to separate paths from revisions, like this:
|
3
|
-
'git <command> [<revision>...] -- [<file>...]'
|
4
|
-
fatal: ambiguous argument 'log': unknown revision or path not in the working tree.
|
5
|
-
Use '--' to separate paths from revisions, like this:
|
6
|
-
'git <command> [<revision>...] -- [<file>...]'
|