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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e74316581d6c5d398840ca97854991e86a18a84f6e24ff8ac17e060e2878371
4
- data.tar.gz: d1224fa3378a9f4bf11800f3c9ed11cfa9f3a0a2002ca0dc92005f5fb1fbb8a8
3
+ metadata.gz: c6106a3dd7bed953c122eab3044de5e900f07db400c1b3791d4140a3f04ff711
4
+ data.tar.gz: f62f3c32cc9c7e5da07bf9748aa98e50041a0414549027ceda1d1afaef0b3294
5
5
  SHA512:
6
- metadata.gz: b80c10dcf884caf541e832d7a36a6a1a4bf5921e4b6893a8f10b6c7ff61c1f19aceb5c421049d4cc20cf5ebb92d2c37fe63d2461cd5d77a848a0494d2500149e
7
- data.tar.gz: 26fd74d4b129cc45acb4c2454ebcc19232af7fae38e519c8ce7cf316c5ed66c32869447c9c7edcf01f23e5b6cc20be813b3d0c262b23d74357d0d061e2277354
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
+ [![Gem Version](https://badge.fury.io/rb/gemlens.svg)](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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #!/usr/bin/env ruby
2
3
 
3
4
  require "bundler/setup"
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 = "gemlens"
7
- s.version = Gemlens::VERSION
8
- s.authors = ["Fai Wong"]
9
- s.email = ["wongwf82@gmail.com"]
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 = "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 and presents a timeline of gem additions, removals, and version updates. It's useful for auditing dependency changes, generating changelogs, and understanding how your project's gem dependencies evolved. Built for maintainers, teams, and curious developers."
13
- s.license = "MIT"
14
- s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
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 = "bin"
25
- s.executables = s.files.grep(%r{\Abin/}) { |f| File.basename(f) }
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"
@@ -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("../../../data/gems.yml", __FILE__)
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 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
22
+ def analyze
23
+ return not_enough_history if gemfile_commits.size < 2
26
24
 
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)
25
+ history = Hash.new { |h, k| h[k] = [] }
29
26
 
30
- # Extract PR number if present
31
- pr = message[/(?:#|PR\s*)(\d+)/i, 1]
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 parse_gemfile_at(commit_sha)
45
- content = Dir.chdir(@repo_path) do
46
- `git show #{commit_sha}:Gemfile`
47
- end
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
- 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
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 analyze
61
- history = Hash.new { |h, k| h[k] = [] }
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
- 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
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
- gemfile_commits.reverse.each_cons(2) do |older, newer|
69
- before = parse_gemfile_at(older[:sha])
70
- after = parse_gemfile_at(newer[:sha])
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
- added = after.keys - before.keys
73
- removed = before.keys - after.keys
74
- common = before.keys & after.keys
86
+ def gemfile_commits
87
+ return [] unless valid_git_repo?
75
88
 
76
- added.each do |gem_name|
77
- history[gem_name] << build_event("added", gem_name, newer.merge(version: after[gem_name]))
78
- end
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
- # 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]
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
- removed.each do |gem_name|
86
- history[gem_name] << build_event("removed", gem_name, newer.merge(
87
- version: before[gem_name]
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
- updated -= removed
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
- 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
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
- history
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, gem_name, commit)
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.print_timeline(history)
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.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
176
+ end
177
+
178
+ flattened.sort_by { |e| e[:date] }
179
+ end
160
180
 
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]}"
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemlens
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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.0
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. Built for maintainers, teams, and curious
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.3.0
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>...]'