gem-why 0.0.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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +162 -0
- data/Rakefile +12 -0
- data/lib/gem_why/analyzer.rb +89 -0
- data/lib/gem_why/formatters/base_formatter.rb +37 -0
- data/lib/gem_why/formatters/deep_formatter.rb +60 -0
- data/lib/gem_why/formatters/direct_formatter.rb +31 -0
- data/lib/gem_why/formatters/tree_formatter.rb +89 -0
- data/lib/gem_why/json_outputter.rb +77 -0
- data/lib/gem_why/tree_builder.rb +38 -0
- data/lib/gem_why/version.rb +5 -0
- data/lib/rubygems/commands/why_command.rb +173 -0
- data/lib/rubygems_plugin.rb +5 -0
- metadata +73 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 427248f88372106860306d895e33b9323628582c6bee05b5b83fed9ce5c1b416
|
|
4
|
+
data.tar.gz: 4e6fec4a6b52226de621ea6d3486e79780c68e3a6e2ba4bcc6e6e843c9bcd0de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8c61e858acfd0fa46812f0eb8548e4710137b07fd9d551ad2dc7a75fe456655bb6c45817a2799e8770d2a4fcc1e59f1bb026ed9ebaa814a090411611d67929cd
|
|
7
|
+
data.tar.gz: bfc45293182646801fc2d3ab7ea7b5f4b464c21c844fd3a7f18743b716e4b4d76b6b0e9e885e036c00774940da1ef6b285b124e10243c4dab296791e4094ec90
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.0.1] - 2025-10-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of `gem-why` plugin
|
|
13
|
+
- `gem why GEMNAME` command to show which installed gems depend on a specific gem
|
|
14
|
+
- Display gem names, versions, and version requirements for dependencies
|
|
15
|
+
- Alphabetically sorted output for easy scanning
|
|
16
|
+
- Clear messaging when no dependencies are found
|
|
17
|
+
- **Deep dependency tracking** - traverse the full dependency tree to find transitive dependencies (DEFAULT)
|
|
18
|
+
- **Tree visualization** with `--tree` option - display dependencies as a visual tree with proper indentation
|
|
19
|
+
- `--direct` option to show only direct dependencies (for quick lookups)
|
|
20
|
+
- Comprehensive test suite with 16 test cases
|
|
21
|
+
- RuboCop compliance with default settings
|
|
22
|
+
- Complete documentation in README
|
|
23
|
+
|
|
24
|
+
[0.0.1]: https://github.com/vitaly/gem-why/releases/tag/v0.0.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vitaly Slobodin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# gem-why
|
|
2
|
+
|
|
3
|
+
A RubyGems plugin that shows which installed gems depend on a specific gem. Similar to `yarn why` or `npm why`.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install the plugin
|
|
9
|
+
gem install gem-why
|
|
10
|
+
|
|
11
|
+
# Find all dependency chains to rake (default - shows transitive dependencies)
|
|
12
|
+
gem why rake
|
|
13
|
+
|
|
14
|
+
# Show only direct dependencies
|
|
15
|
+
gem why rake --direct
|
|
16
|
+
|
|
17
|
+
# Visualize dependencies as a tree
|
|
18
|
+
gem why rake --tree
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why Deep by Default?
|
|
22
|
+
|
|
23
|
+
Unlike most dependency tools that show only direct dependencies by default, `gem why` defaults to **deep dependency analysis** because:
|
|
24
|
+
|
|
25
|
+
- **Most real-world scenarios** involve transitive dependencies - you typically want to know "why is this gem even installed?" not just "what directly depends on it?"
|
|
26
|
+
- **Debugging is easier** when you see the full chain immediately - you can trace exactly how a problematic gem entered your dependency tree
|
|
27
|
+
- **Mirrors `yarn why`** behavior - the inspiration for this tool also defaults to showing full dependency chains
|
|
28
|
+
- **Direct lookups are still fast** - use `--direct` when you specifically need just immediate dependents
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install the gem by executing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
gem install gem-why
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The plugin will automatically be available as a `gem` command after installation.
|
|
39
|
+
|
|
40
|
+
## Feature Comparison
|
|
41
|
+
|
|
42
|
+
| Mode | Command | Shows | Best For |
|
|
43
|
+
|------|---------|-------|----------|
|
|
44
|
+
| **Deep** (default) | `gem why GEMNAME` | Full dependency chains with paths | Understanding transitive dependencies |
|
|
45
|
+
| **Direct** | `gem why GEMNAME --direct` | Only direct dependencies | Quick lookup of immediate dependents |
|
|
46
|
+
| **Tree** | `gem why GEMNAME --tree` | Visual tree structure | Complex dependency visualization |
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
To see which gems depend on a specific gem:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
gem why GEMNAME [options]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Examples
|
|
57
|
+
|
|
58
|
+
#### Default Behavior - Deep Dependencies
|
|
59
|
+
|
|
60
|
+
Show all dependency chains leading to a gem (including transitive dependencies):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ gem why concurrent-ruby
|
|
64
|
+
Dependency chains leading to concurrent-ruby:
|
|
65
|
+
|
|
66
|
+
rails => activesupport => concurrent-ruby
|
|
67
|
+
└─ rails (8.0.3) requires activesupport = 8.0.3
|
|
68
|
+
└─ activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
|
|
69
|
+
|
|
70
|
+
actionpack => activesupport => concurrent-ruby
|
|
71
|
+
└─ actionpack (8.0.3) requires activesupport = 8.0.3
|
|
72
|
+
└─ activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
|
|
73
|
+
|
|
74
|
+
Total: 10 root gem(s) depend on concurrent-ruby
|
|
75
|
+
Found 25 dependency chain(s)
|
|
76
|
+
|
|
77
|
+
Tip: Use --direct for direct dependencies only or --tree for a visual tree
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Direct Dependencies Only
|
|
81
|
+
|
|
82
|
+
Check which gems directly depend on `rake`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
$ gem why rake --direct
|
|
86
|
+
Gems that depend on rake:
|
|
87
|
+
|
|
88
|
+
ast (2.4.3) requires rake ~> 13.2
|
|
89
|
+
minitest (5.16.0) requires rake >= 0
|
|
90
|
+
rubocop (1.21.0) requires rake >= 0
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
Total: 15 gem(s)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Tree Visualization
|
|
97
|
+
|
|
98
|
+
Display dependencies as a visual tree:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
$ gem why concurrent-ruby --tree
|
|
102
|
+
Dependency tree for concurrent-ruby:
|
|
103
|
+
|
|
104
|
+
rails (8.0.3)
|
|
105
|
+
├── activesupport = 8.0.3
|
|
106
|
+
│ └── activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
|
|
107
|
+
│ └── concurrent-ruby ✓
|
|
108
|
+
|
|
109
|
+
actionpack (8.0.3)
|
|
110
|
+
├── activesupport = 8.0.3
|
|
111
|
+
│ └── activesupport (8.0.3) requires concurrent-ruby ~> 1.0, >= 1.3.1
|
|
112
|
+
│ └── concurrent-ruby ✓
|
|
113
|
+
|
|
114
|
+
Total: 10 root gem(s) depend on concurrent-ruby
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If no gems depend on the specified gem:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
$ gem why nonexistent-gem
|
|
121
|
+
No gems depend on nonexistent-gem
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Options
|
|
125
|
+
|
|
126
|
+
| Option | Short | Description |
|
|
127
|
+
|--------|-------|-------------|
|
|
128
|
+
| (none) | | Show full dependency chains (default) |
|
|
129
|
+
| `--direct` | `-d` | Show only direct dependencies |
|
|
130
|
+
| `--tree` | `-t` | Display as a visual tree |
|
|
131
|
+
| `--help` | `-h` | Show help message |
|
|
132
|
+
| `--verbose` | `-V` | Verbose output |
|
|
133
|
+
|
|
134
|
+
## How It Works
|
|
135
|
+
|
|
136
|
+
### Default Mode (Deep Dependencies)
|
|
137
|
+
|
|
138
|
+
1. Builds a complete dependency graph of all installed gems
|
|
139
|
+
2. Finds all paths (dependency chains) leading to the target gem
|
|
140
|
+
3. Shows transitive dependencies (A depends on B, B depends on target)
|
|
141
|
+
4. Groups results by root gems
|
|
142
|
+
5. Displays the full chain: `root => intermediate => target`
|
|
143
|
+
|
|
144
|
+
### Direct Mode (`--direct`)
|
|
145
|
+
|
|
146
|
+
1. Scans all installed gems on your system
|
|
147
|
+
2. Checks both runtime and development dependencies
|
|
148
|
+
3. Finds which gems **directly** depend on the specified gem
|
|
149
|
+
4. Displays results with gem names, versions, and requirements
|
|
150
|
+
5. Results are sorted alphabetically for easy reading
|
|
151
|
+
|
|
152
|
+
### Tree Mode (`--tree`)
|
|
153
|
+
|
|
154
|
+
1. Performs deep dependency analysis
|
|
155
|
+
2. Builds a hierarchical tree structure
|
|
156
|
+
3. Visualizes dependencies using tree characters (├──, └──, │)
|
|
157
|
+
4. Shows the complete dependency path from root to target
|
|
158
|
+
5. Makes it easy to understand complex dependency relationships
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemWhy
|
|
4
|
+
# Analyzes gem dependencies to find dependents and dependency chains
|
|
5
|
+
class Analyzer
|
|
6
|
+
# Finds all gems that directly depend on the target gem
|
|
7
|
+
# @param target_gem_name [String] the gem to find dependents for
|
|
8
|
+
# @return [Array<Array(String, String)>] array of [gem_name, requirement] pairs
|
|
9
|
+
def find_direct_dependents(target_gem_name)
|
|
10
|
+
dependents = []
|
|
11
|
+
normalized_target = target_gem_name.downcase
|
|
12
|
+
|
|
13
|
+
Gem::Specification.each do |spec|
|
|
14
|
+
spec.dependencies.each do |dep|
|
|
15
|
+
dependents << [spec.name, dep.requirement.to_s] if dep.name.downcase == normalized_target
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
dependents.sort_by(&:first)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Finds all dependency chains leading to the target gem
|
|
23
|
+
# @param target_gem_name [String] the gem to find chains for
|
|
24
|
+
# @return [Array<Array<Hash>>] array of dependency chains
|
|
25
|
+
def find_dependency_chains(target_gem_name)
|
|
26
|
+
chains = []
|
|
27
|
+
normalized_target = target_gem_name.downcase
|
|
28
|
+
|
|
29
|
+
Gem::Specification.each do |spec|
|
|
30
|
+
paths = find_paths_to_target(spec.name, normalized_target, [])
|
|
31
|
+
chains.concat(paths)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
chains.uniq.sort_by { |chain| chain.first[:name] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Recursively finds all paths from current gem to target gem
|
|
40
|
+
# @param current_gem [String] the current gem being explored
|
|
41
|
+
# @param target_gem [String] the gem we're searching for
|
|
42
|
+
# @param path [Array<Hash>] the current dependency path
|
|
43
|
+
# @param visited [Set<String>] set of already visited gems (prevents cycles)
|
|
44
|
+
# @return [Array<Array<Hash>>] array of paths to the target
|
|
45
|
+
def find_paths_to_target(current_gem, target_gem, path, visited = Set.new)
|
|
46
|
+
return [] if visited.include?(current_gem)
|
|
47
|
+
|
|
48
|
+
visited = visited.dup
|
|
49
|
+
visited.add(current_gem)
|
|
50
|
+
spec = load_gem_spec(current_gem)
|
|
51
|
+
return [] unless spec
|
|
52
|
+
|
|
53
|
+
collect_dependency_paths(spec, target_gem, path, visited)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def collect_dependency_paths(spec, target_gem, path, visited)
|
|
57
|
+
paths = []
|
|
58
|
+
|
|
59
|
+
spec.dependencies.each do |dep|
|
|
60
|
+
new_node = build_dependency_node(spec, dep)
|
|
61
|
+
paths.concat(process_dependency(dep, target_gem, path, new_node, visited))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
paths
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def process_dependency(dep, target_gem, path, new_node, visited)
|
|
68
|
+
if dep.name.downcase == target_gem.downcase
|
|
69
|
+
[path + [new_node]]
|
|
70
|
+
else
|
|
71
|
+
find_paths_to_target(dep.name, target_gem, path + [new_node], visited)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def load_gem_spec(gem_name)
|
|
76
|
+
Gem::Specification.find_by_name(gem_name)
|
|
77
|
+
rescue Gem::MissingSpecError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_dependency_node(spec, dep)
|
|
82
|
+
name = spec.name
|
|
83
|
+
version = spec.version.to_s
|
|
84
|
+
dependency = dep.name
|
|
85
|
+
requirement = dep.requirement.to_s
|
|
86
|
+
{ name:, version:, dependency:, requirement: }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rainbow"
|
|
4
|
+
|
|
5
|
+
module GemWhy
|
|
6
|
+
module Formatters
|
|
7
|
+
# Base formatter with colorization support
|
|
8
|
+
class BaseFormatter
|
|
9
|
+
attr_reader :command
|
|
10
|
+
|
|
11
|
+
def initialize(command)
|
|
12
|
+
@command = command
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Determines if output should be colorized
|
|
18
|
+
# @return [Boolean] true if colors should be used
|
|
19
|
+
def colorize?
|
|
20
|
+
!command.options[:no_color] && $stdout.tty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Colorizes text if appropriate
|
|
24
|
+
# @param text [String] the text to colorize
|
|
25
|
+
# @param color [Symbol] the color to apply
|
|
26
|
+
# @return [String] the colorized or original text
|
|
27
|
+
def colorize(text, color)
|
|
28
|
+
colorize? ? Rainbow(text).color(color) : text
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Delegates to command's say method
|
|
32
|
+
def say(message)
|
|
33
|
+
command.say(message)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module GemWhy
|
|
6
|
+
module Formatters
|
|
7
|
+
# Formats deep dependency chains output
|
|
8
|
+
class DeepFormatter < BaseFormatter
|
|
9
|
+
# Formats and displays deep dependency chains
|
|
10
|
+
# @param gem_name [String] the target gem name
|
|
11
|
+
# @param chains [Array<Array<Hash>>] the dependency chains
|
|
12
|
+
# @return [void]
|
|
13
|
+
def format(gem_name, chains)
|
|
14
|
+
return say "No gems depend on #{colorize(gem_name, :yellow)}" if chains.empty?
|
|
15
|
+
|
|
16
|
+
say "Dependency chains leading to #{colorize(gem_name, :cyan)}:\n\n"
|
|
17
|
+
print_dependency_chains(chains, gem_name)
|
|
18
|
+
print_deep_summary(chains, gem_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def print_dependency_chains(chains, gem_name)
|
|
24
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
25
|
+
|
|
26
|
+
chains_by_root.each_value do |root_chains|
|
|
27
|
+
root_chains.each do |chain|
|
|
28
|
+
display_chain(chain, gem_name)
|
|
29
|
+
say ""
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print_deep_summary(chains, gem_name)
|
|
35
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
36
|
+
root_gems = chains_by_root.keys
|
|
37
|
+
|
|
38
|
+
say "#{colorize("Total:", :green)} #{root_gems.size} root gem(s) depend on #{gem_name}"
|
|
39
|
+
say "Found #{chains.size} dependency chain(s)"
|
|
40
|
+
say "\n#{colorize("Tip:", :yellow)} Use --direct for direct dependencies only or --tree for a visual tree"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def display_chain(chain, gem_name)
|
|
44
|
+
path_str = chain.map { |node| colorize(node[:name], :blue) }.join(" #{colorize("=>", :white)} ")
|
|
45
|
+
path_str += " #{colorize("=>", :white)} #{colorize(gem_name, :cyan)}"
|
|
46
|
+
say " #{path_str}"
|
|
47
|
+
|
|
48
|
+
chain.each_with_index do |node, idx|
|
|
49
|
+
display_chain_node(node, idx)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def display_chain_node(node, idx)
|
|
54
|
+
indent = " " * (idx + 1)
|
|
55
|
+
requirement = "#{node[:dependency]} #{node[:requirement]}"
|
|
56
|
+
say "#{indent}└─ #{node[:name]} (#{node[:version]}) requires #{requirement}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module GemWhy
|
|
6
|
+
module Formatters
|
|
7
|
+
# Formats direct dependencies output
|
|
8
|
+
class DirectFormatter < BaseFormatter
|
|
9
|
+
# Formats and displays direct dependencies
|
|
10
|
+
# @param gem_name [String] the target gem name
|
|
11
|
+
# @param dependents [Array<Array(String, String)>] the dependent gems
|
|
12
|
+
# @return [void]
|
|
13
|
+
def format(gem_name, dependents)
|
|
14
|
+
return say "No gems depend on #{colorize(gem_name, :yellow)}" if dependents.empty?
|
|
15
|
+
|
|
16
|
+
say "Gems that depend on #{colorize(gem_name, :cyan)}:\n\n"
|
|
17
|
+
print_direct_dependents(dependents, gem_name)
|
|
18
|
+
say "\n#{colorize("Total:", :green)} #{dependents.size} gem(s)"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def print_direct_dependents(dependents, gem_name)
|
|
24
|
+
dependents.each do |dependent_name, requirement|
|
|
25
|
+
spec = Gem::Specification.find_by_name(dependent_name)
|
|
26
|
+
say " #{colorize(dependent_name, :blue)} (#{spec.version}) requires #{gem_name} #{requirement}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_formatter"
|
|
4
|
+
|
|
5
|
+
module GemWhy
|
|
6
|
+
module Formatters
|
|
7
|
+
# Formats tree visualization output
|
|
8
|
+
class TreeFormatter < BaseFormatter
|
|
9
|
+
attr_reader :tree_builder
|
|
10
|
+
|
|
11
|
+
def initialize(command, tree_builder)
|
|
12
|
+
super(command)
|
|
13
|
+
@tree_builder = tree_builder
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Formats and displays dependency tree
|
|
17
|
+
# @param gem_name [String] the target gem name
|
|
18
|
+
# @param chains [Array<Array<Hash>>] the dependency chains
|
|
19
|
+
# @return [void]
|
|
20
|
+
def format(gem_name, chains)
|
|
21
|
+
return say "No gems depend on #{colorize(gem_name, :yellow)}" if chains.empty?
|
|
22
|
+
|
|
23
|
+
say "Dependency tree for #{colorize(gem_name, :cyan)}:\n\n"
|
|
24
|
+
print_dependency_tree(chains, gem_name)
|
|
25
|
+
print_tree_summary(chains, gem_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def print_dependency_tree(chains, gem_name)
|
|
31
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
32
|
+
|
|
33
|
+
chains_by_root.each do |root_name, root_chains|
|
|
34
|
+
print_root_tree(root_name, root_chains, gem_name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def print_root_tree(root_name, root_chains, gem_name)
|
|
39
|
+
root_spec = Gem::Specification.find_by_name(root_name)
|
|
40
|
+
say "#{colorize(root_name, :blue)} (#{root_spec.version})"
|
|
41
|
+
|
|
42
|
+
tree = tree_builder.build_tree_structure(root_chains)
|
|
43
|
+
display_tree_node(tree, "", gem_name)
|
|
44
|
+
|
|
45
|
+
say ""
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print_tree_summary(chains, gem_name)
|
|
49
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
50
|
+
root_gems = chains_by_root.keys
|
|
51
|
+
say "#{colorize("Total:", :green)} #{root_gems.size} root gem(s) depend on #{gem_name}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def display_tree_node(tree, prefix, target_gem, depth = 0)
|
|
55
|
+
return if tree.empty?
|
|
56
|
+
|
|
57
|
+
tree.each_with_index do |(key, value), index|
|
|
58
|
+
is_last = index == tree.size - 1
|
|
59
|
+
display_node_line(key, value, prefix, is_last, depth)
|
|
60
|
+
display_children_or_target(value, prefix, is_last, target_gem, depth)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def display_node_line(key, value, prefix, is_last, depth)
|
|
65
|
+
connector = is_last ? "└──" : "├──"
|
|
66
|
+
line = build_node_line(key, value, depth)
|
|
67
|
+
say "#{prefix}#{connector} #{line}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_node_line(key, value, depth)
|
|
71
|
+
if depth.zero?
|
|
72
|
+
"#{value[:dependency]} #{value[:requirement]}"
|
|
73
|
+
else
|
|
74
|
+
"#{key} requires #{value[:dependency]} #{value[:requirement]}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def display_children_or_target(value, prefix, is_last, target_gem, depth)
|
|
79
|
+
new_prefix = prefix + (is_last ? " " : "│ ")
|
|
80
|
+
|
|
81
|
+
if value[:children].empty?
|
|
82
|
+
say "#{new_prefix}└── #{colorize(target_gem, :cyan)} #{colorize("✓", :green)}"
|
|
83
|
+
else
|
|
84
|
+
display_tree_node(value[:children], new_prefix, target_gem, depth + 1)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module GemWhy
|
|
6
|
+
# Handles JSON output for all display modes
|
|
7
|
+
class JSONOutputter
|
|
8
|
+
attr_reader :command, :tree_builder
|
|
9
|
+
|
|
10
|
+
def initialize(command, tree_builder)
|
|
11
|
+
@command = command
|
|
12
|
+
@tree_builder = tree_builder
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Outputs direct dependencies as JSON
|
|
16
|
+
# @param gem_name [String] the target gem name
|
|
17
|
+
# @param dependents [Array<Array(String, String)>] the dependent gems
|
|
18
|
+
# @return [void]
|
|
19
|
+
def output_direct(gem_name, dependents)
|
|
20
|
+
target = gem_name
|
|
21
|
+
mode = "direct"
|
|
22
|
+
total = dependents.size
|
|
23
|
+
dependents_data = dependents.map do |name, requirement|
|
|
24
|
+
spec = Gem::Specification.find_by_name(name)
|
|
25
|
+
version = spec.version.to_s
|
|
26
|
+
{ name:, version:, requirement: }
|
|
27
|
+
end
|
|
28
|
+
output = { target:, mode:, dependents: dependents_data, total: }
|
|
29
|
+
say JSON.pretty_generate(output)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Outputs deep dependency chains as JSON
|
|
33
|
+
# @param gem_name [String] the target gem name
|
|
34
|
+
# @param chains [Array<Array<Hash>>] the dependency chains
|
|
35
|
+
# @return [void]
|
|
36
|
+
def output_deep(gem_name, chains)
|
|
37
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
38
|
+
target = gem_name
|
|
39
|
+
mode = "deep"
|
|
40
|
+
chains_data = chains.map { |chain| chain.map { |node| node.slice(:name, :version, :dependency, :requirement) } }
|
|
41
|
+
root_gems = chains_by_root.keys.size
|
|
42
|
+
total_chains = chains.size
|
|
43
|
+
output = { target:, mode:, chains: chains_data, root_gems:, total_chains: }
|
|
44
|
+
say JSON.pretty_generate(output)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Outputs dependency tree as JSON
|
|
48
|
+
# @param gem_name [String] the target gem name
|
|
49
|
+
# @param chains [Array<Array<Hash>>] the dependency chains
|
|
50
|
+
# @return [void]
|
|
51
|
+
def output_tree(gem_name, chains)
|
|
52
|
+
chains_by_root = chains.group_by { |chain| chain.first[:name] }
|
|
53
|
+
target = gem_name
|
|
54
|
+
mode = "tree"
|
|
55
|
+
roots = build_json_roots(chains_by_root)
|
|
56
|
+
total_roots = chains_by_root.keys.size
|
|
57
|
+
output = { target:, mode:, roots:, total_roots: }
|
|
58
|
+
say JSON.pretty_generate(output)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_json_roots(chains_by_root)
|
|
64
|
+
chains_by_root.map do |root_name, root_chains|
|
|
65
|
+
root_spec = Gem::Specification.find_by_name(root_name)
|
|
66
|
+
name = root_name
|
|
67
|
+
version = root_spec.version.to_s
|
|
68
|
+
tree = tree_builder.build_tree_structure(root_chains)
|
|
69
|
+
{ name:, version:, tree: }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def say(message)
|
|
74
|
+
command.say(message)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemWhy
|
|
4
|
+
# Builds tree structures from dependency chains
|
|
5
|
+
class TreeBuilder
|
|
6
|
+
# Builds a tree structure from dependency chains
|
|
7
|
+
# @param chains [Array<Array<Hash>>] the dependency chains
|
|
8
|
+
# @return [Hash] hierarchical tree structure
|
|
9
|
+
def build_tree_structure(chains)
|
|
10
|
+
tree = {}
|
|
11
|
+
|
|
12
|
+
chains.each do |chain|
|
|
13
|
+
add_chain_to_tree(tree, chain)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
tree
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def add_chain_to_tree(tree, chain)
|
|
22
|
+
current_level = tree
|
|
23
|
+
|
|
24
|
+
chain.each do |node|
|
|
25
|
+
key = "#{node[:name]} (#{node[:version]})"
|
|
26
|
+
current_level[key] ||= create_tree_node(node)
|
|
27
|
+
current_level = current_level[key][:children]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_tree_node(node)
|
|
32
|
+
dependency = node[:dependency]
|
|
33
|
+
requirement = node[:requirement]
|
|
34
|
+
children = {}
|
|
35
|
+
{ dependency:, requirement:, children: }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems/command"
|
|
4
|
+
require_relative "../../gem_why/version"
|
|
5
|
+
require_relative "../../gem_why/analyzer"
|
|
6
|
+
require_relative "../../gem_why/tree_builder"
|
|
7
|
+
require_relative "../../gem_why/json_outputter"
|
|
8
|
+
require_relative "../../gem_why/formatters/direct_formatter"
|
|
9
|
+
require_relative "../../gem_why/formatters/deep_formatter"
|
|
10
|
+
require_relative "../../gem_why/formatters/tree_formatter"
|
|
11
|
+
|
|
12
|
+
module Gem
|
|
13
|
+
module Commands
|
|
14
|
+
# Command to show which gems depend on a specific gem
|
|
15
|
+
#
|
|
16
|
+
# This command helps identify dependency relationships by showing:
|
|
17
|
+
# - Direct dependencies (--direct): immediate dependents only
|
|
18
|
+
# - Deep dependencies (default): full dependency chains
|
|
19
|
+
# - Tree visualization (--tree): hierarchical view
|
|
20
|
+
#
|
|
21
|
+
# @example Show all dependency chains
|
|
22
|
+
# gem why concurrent-ruby
|
|
23
|
+
#
|
|
24
|
+
# @example Show only direct dependencies
|
|
25
|
+
# gem why rake --direct
|
|
26
|
+
#
|
|
27
|
+
# @example Show tree visualization
|
|
28
|
+
# gem why concurrent-ruby --tree
|
|
29
|
+
#
|
|
30
|
+
# @example Output as JSON
|
|
31
|
+
# gem why rake --json
|
|
32
|
+
class WhyCommand < Gem::Command
|
|
33
|
+
# Initializes the why command with options
|
|
34
|
+
def initialize
|
|
35
|
+
super("why", "Show which gems depend on a specific gem")
|
|
36
|
+
setup_options
|
|
37
|
+
initialize_dependencies
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String] long description of the command
|
|
41
|
+
def description
|
|
42
|
+
"Show which installed gems depend on a specific gem, including dependency chains"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] usage string for the command
|
|
46
|
+
def usage
|
|
47
|
+
"#{program_name} GEMNAME [options]"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Executes the command with the provided arguments
|
|
51
|
+
# @return [void]
|
|
52
|
+
def execute
|
|
53
|
+
gem_name = validate_gem_name
|
|
54
|
+
route_to_display_mode(gem_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :analyzer, :tree_builder, :json_outputter, :direct_formatter, :deep_formatter, :tree_formatter
|
|
60
|
+
|
|
61
|
+
def setup_options
|
|
62
|
+
setup_tree_option
|
|
63
|
+
setup_direct_option
|
|
64
|
+
setup_version_option
|
|
65
|
+
setup_no_color_option
|
|
66
|
+
setup_json_option
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def setup_tree_option
|
|
70
|
+
add_option("-t", "--tree", "Display dependencies as a tree") do |value, options|
|
|
71
|
+
options[:tree] = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def setup_direct_option
|
|
76
|
+
add_option("-d", "--direct", "Show only direct dependencies") do |value, options|
|
|
77
|
+
options[:direct] = value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def setup_version_option
|
|
82
|
+
add_option("-v", "--version", "Show gem-why version") do |_value, _options|
|
|
83
|
+
say "gem-why version #{GemWhy::VERSION}"
|
|
84
|
+
terminate_interaction
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def setup_no_color_option
|
|
89
|
+
add_option("--no-color", "Disable colored output") do |_value, options|
|
|
90
|
+
options[:no_color] = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def setup_json_option
|
|
95
|
+
add_option("--json", "Output in JSON format") do |_value, options|
|
|
96
|
+
options[:json] = true
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def initialize_dependencies
|
|
101
|
+
@analyzer = GemWhy::Analyzer.new
|
|
102
|
+
@tree_builder = GemWhy::TreeBuilder.new
|
|
103
|
+
@json_outputter = GemWhy::JSONOutputter.new(self, tree_builder)
|
|
104
|
+
@direct_formatter = GemWhy::Formatters::DirectFormatter.new(self)
|
|
105
|
+
@deep_formatter = GemWhy::Formatters::DeepFormatter.new(self)
|
|
106
|
+
@tree_formatter = GemWhy::Formatters::TreeFormatter.new(self, tree_builder)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validates and normalizes the gem name argument
|
|
110
|
+
# @return [String] the normalized gem name
|
|
111
|
+
# @raise [Gem::CommandLineError] if no gem name provided
|
|
112
|
+
def validate_gem_name
|
|
113
|
+
gem_name = get_one_optional_argument || raise(
|
|
114
|
+
Gem::CommandLineError,
|
|
115
|
+
"Please specify a gem name (e.g. gem why rake)"
|
|
116
|
+
)
|
|
117
|
+
gem_name.downcase
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Routes execution to the appropriate display mode
|
|
121
|
+
# @param gem_name [String] the target gem name
|
|
122
|
+
# @return [void]
|
|
123
|
+
def route_to_display_mode(gem_name)
|
|
124
|
+
if options[:direct]
|
|
125
|
+
show_direct_dependencies(gem_name)
|
|
126
|
+
elsif options[:tree]
|
|
127
|
+
show_tree_visualization(gem_name)
|
|
128
|
+
else
|
|
129
|
+
show_deep_dependencies(gem_name)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Shows direct dependencies only
|
|
134
|
+
# @param gem_name [String] the target gem name
|
|
135
|
+
# @return [void]
|
|
136
|
+
def show_direct_dependencies(gem_name)
|
|
137
|
+
dependents = analyzer.find_direct_dependents(gem_name)
|
|
138
|
+
|
|
139
|
+
if options[:json]
|
|
140
|
+
json_outputter.output_direct(gem_name, dependents)
|
|
141
|
+
else
|
|
142
|
+
direct_formatter.format(gem_name, dependents)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Shows deep dependencies (full chains)
|
|
147
|
+
# @param gem_name [String] the target gem name
|
|
148
|
+
# @return [void]
|
|
149
|
+
def show_deep_dependencies(gem_name)
|
|
150
|
+
chains = analyzer.find_dependency_chains(gem_name)
|
|
151
|
+
|
|
152
|
+
if options[:json]
|
|
153
|
+
json_outputter.output_deep(gem_name, chains)
|
|
154
|
+
else
|
|
155
|
+
deep_formatter.format(gem_name, chains)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Shows tree visualization
|
|
160
|
+
# @param gem_name [String] the target gem name
|
|
161
|
+
# @return [void]
|
|
162
|
+
def show_tree_visualization(gem_name)
|
|
163
|
+
chains = analyzer.find_dependency_chains(gem_name)
|
|
164
|
+
|
|
165
|
+
if options[:json]
|
|
166
|
+
json_outputter.output_tree(gem_name, chains)
|
|
167
|
+
else
|
|
168
|
+
tree_formatter.format(gem_name, chains)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gem-why
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vitaly Slobodin
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rainbow
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.1'
|
|
26
|
+
description: A RubyGems plugin that shows which gems depend on a specific gem, including
|
|
27
|
+
full dependency chains and tree visualization.
|
|
28
|
+
email:
|
|
29
|
+
- vitaliy.slobodin@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/gem_why/analyzer.rb
|
|
39
|
+
- lib/gem_why/formatters/base_formatter.rb
|
|
40
|
+
- lib/gem_why/formatters/deep_formatter.rb
|
|
41
|
+
- lib/gem_why/formatters/direct_formatter.rb
|
|
42
|
+
- lib/gem_why/formatters/tree_formatter.rb
|
|
43
|
+
- lib/gem_why/json_outputter.rb
|
|
44
|
+
- lib/gem_why/tree_builder.rb
|
|
45
|
+
- lib/gem_why/version.rb
|
|
46
|
+
- lib/rubygems/commands/why_command.rb
|
|
47
|
+
- lib/rubygems_plugin.rb
|
|
48
|
+
homepage: https://github.com/vitallium/gem-why
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
homepage_uri: https://github.com/vitallium/gem-why
|
|
53
|
+
source_code_uri: https://github.com/vitallium/gem-why
|
|
54
|
+
changelog_uri: https://github.com/vitallium/gem-why/blob/main/CHANGELOG.md
|
|
55
|
+
rubygems_mfa_required: 'true'
|
|
56
|
+
rdoc_options: []
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 3.2.0
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
requirements: []
|
|
70
|
+
rubygems_version: 3.6.9
|
|
71
|
+
specification_version: 4
|
|
72
|
+
summary: A RubyGems plugin to show which gems depend on a specific gem
|
|
73
|
+
test_files: []
|