whatthegem 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/README.md +56 -0
- data/exe/whatthegem +20 -0
- data/lib/whatthegem/changes/markdown_parser.rb +41 -0
- data/lib/whatthegem/changes/parser.rb +42 -0
- data/lib/whatthegem/changes/rdoc_parser.rb +42 -0
- data/lib/whatthegem/changes/releases_parser.rb +27 -0
- data/lib/whatthegem/changes.rb +120 -0
- data/lib/whatthegem/commands.rb +101 -0
- data/lib/whatthegem/core_ext.rb +30 -0
- data/lib/whatthegem/gem/bundled.rb +55 -0
- data/lib/whatthegem/gem/github.rb +94 -0
- data/lib/whatthegem/gem/rubygems.rb +29 -0
- data/lib/whatthegem/gem.rb +64 -0
- data/lib/whatthegem/help.rb +31 -0
- data/lib/whatthegem/hobject.rb +61 -0
- data/lib/whatthegem/i.rb +56 -0
- data/lib/whatthegem/info.rb +58 -0
- data/lib/whatthegem/stats/definitions.rb +50 -0
- data/lib/whatthegem/stats/meters.rb +78 -0
- data/lib/whatthegem/stats.rb +12 -0
- data/lib/whatthegem/template.rb +53 -0
- data/lib/whatthegem/tty-markdown_patch.rb +9 -0
- data/lib/whatthegem/usage/extractor.rb +104 -0
- data/lib/whatthegem/usage.rb +47 -0
- data/lib/whatthegem.rb +12 -0
- metadata +251 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c9f1d103cc46e0222855e20a653a1f0d6035d706
|
4
|
+
data.tar.gz: 482bb2927a0e82b9125bd5d0805da02073536025
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c26011bb1bc4043411507d0faf444942de248b92af28c842e035aa316e443d2ef5ba2c142569034a41ebffdb9d8f3a9a132785c7add87bf7b8bacae49a05960b
|
7
|
+
data.tar.gz: ce913168e10295374f15a3ae09cd6f7d0a6423a6c42917af36c4ac9581c9b3d540f80f441aa093fd7487e9cfd3b25b687c85d72279d1367069198304438d5d62
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
[](http://badge.fury.io/rb/whatthegem)
|
2
|
+
|
3
|
+
`whatthegem` is a small utility to answer some questions about Ruby gems you work with or planning to work with.
|
4
|
+
|
5
|
+
It tries to answer—**right in your terminal**—questions like the following:
|
6
|
+
|
7
|
+
* Colleague added `gemname` to our `Gemfile`, what is it?
|
8
|
+
* How outdated is my favorite `gemname` I am using locally, what's changed since then?
|
9
|
+
* What's that benchmarking `gemname`'s synopsis was, again?
|
10
|
+
* There is `gemname` advised on the internetz for my problem, is it still maintained? Is it widely used?
|
11
|
+
|
12
|
+
There are a lot of ways to answer those questions through Google and various sites, but if you are in a terminal currently, `whatthegem` is fastest, most focused and convenient.
|
13
|
+
|
14
|
+
Showcase:
|
15
|
+
|
16
|
+
`gem install whatthegem`
|
17
|
+
|
18
|
+
## `whatthegem <gem> info`
|
19
|
+
|
20
|
+

|
21
|
+
|
22
|
+
Just extracts gem's description and version from RubyGems.org and presents it to you alongside your local versions.
|
23
|
+
|
24
|
+
## `whatthegem <gem> usage`
|
25
|
+
|
26
|
+

|
27
|
+
|
28
|
+
Tries to parse gem's GitHub README (or local README, if the gem is installed and includes it), and extract Ruby code blocks from there (except trivial, like "Add to your `Gemfile`: `gem 'gemname'`) and prints them. Typically, it is the main/most basic usage examples.
|
29
|
+
|
30
|
+
## `whatthegem <gem> changes`
|
31
|
+
|
32
|
+

|
33
|
+
|
34
|
+
Parses gem's GitHub `Changelog.md`/`NEWS`/`History`, or GitHub releases description, and lists versions and their changes (up to your local version, if it is older than recent one).
|
35
|
+
|
36
|
+
## `whatthegem <gem> stats`
|
37
|
+
|
38
|
+

|
39
|
+
|
40
|
+
Some statistics about gem's maintenance, freshness, and popularity (again, from RubyGems.org and GitHub). It doesn't _judge_, just provides helpful quick insights.
|
41
|
+
|
42
|
+
> Note that, for example, "not having a new version in last year or two" doesn't necessary means the gem is abandoned, it could be just "complete".
|
43
|
+
|
44
|
+
_This functionality is ported from my earlier gem [any_good](https://github.com/zverok/any_good)—after its creation, I eventually understood there are several things I'd like to be able to know about the gems, and so whatthegem was born._
|
45
|
+
|
46
|
+
## Limitations
|
47
|
+
|
48
|
+
* As significant amount of information is taken from GitHub, `whatthegem` is less useful for gems that aren't on GitHub, or doesn't specify their repo URL in gemspec (available via RubyGems.org API); integrating of other source hosting is possible, but currently, in my experience, it seems like \~99% of gems are there;
|
49
|
+
* Also, GitHub usage/changes extraction for gems that aren't in the root of the repo (like [ActiveRecord](https://github.com/rails/rails/tree/v5.2.3/activerecord)) aren't supported yet;
|
50
|
+
* As `usage` and `changes` are extracted by heuristics, it doesn't always work well (though it **does** in a surprisingly large number of cases);
|
51
|
+
* For example, `usage` is typically informative for stand-alone libraries, but hardly so for Rails plugins (which have instructions like "run this generator, add that to config, insert this line in the model" in their README)
|
52
|
+
* For other, not all gems have their Changelog findable, or parseable.
|
53
|
+
|
54
|
+
## Author
|
55
|
+
|
56
|
+
[@zverok](https://zverok.github.io)
|
data/exe/whatthegem
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.expand_path '../lib', __dir__
|
3
|
+
|
4
|
+
require 'whatthegem'
|
5
|
+
|
6
|
+
name, cmd, *args = ARGV
|
7
|
+
|
8
|
+
# FIXME: Currently, there is no gem named `help`, but what if it would be?
|
9
|
+
# Maybe it is a reserved name?
|
10
|
+
if %w[help h --help -h].include?(name) || name.nil?
|
11
|
+
WhatTheGem::Help.call
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
cmd ||= 'info'
|
16
|
+
gem = WhatTheGem::Gem.fetch(name)
|
17
|
+
|
18
|
+
WhatTheGem::Command.get(cmd)
|
19
|
+
.then { |command| command || WhatTheGem::Help }
|
20
|
+
.call(gem, *args)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Changes
|
3
|
+
class MarkdownParser < Parser
|
4
|
+
def versions
|
5
|
+
nodes = I::Kramdowns.elements(content)
|
6
|
+
level = detect_version_level(nodes) # find level of headers which contain version
|
7
|
+
|
8
|
+
sections = sections(nodes, level)
|
9
|
+
.select { |title,| title.match?(VERSION_LINE_REGEXP) }
|
10
|
+
.map(&method(:make_version))
|
11
|
+
.sort_by(&:number) # it is internally converted to Gem::Version, so "1.12" is correctly > "1.2"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def sections(nodes, level)
|
17
|
+
nodes
|
18
|
+
.chunk { |n| n.type == :header && n.options[:level] == level } # chunk into sections by header
|
19
|
+
.drop_while { |header,| !header } # drop before first header
|
20
|
+
.map(&:last) # drop `true`, `false` flags after chunk
|
21
|
+
.each_slice(2) # join header with subsequent nodes
|
22
|
+
.map { |(h, *), nodes| [h.options[:raw_text], nodes] }
|
23
|
+
end
|
24
|
+
|
25
|
+
def detect_version_level(nodes)
|
26
|
+
nodes
|
27
|
+
.select { |n| n.type == :header && n.options[:raw_text].match?(VERSION_LINE_REGEXP) }
|
28
|
+
.map { |n| n.options[:level] }.min
|
29
|
+
end
|
30
|
+
|
31
|
+
def make_version((title, nodes))
|
32
|
+
# TODO: date, if known
|
33
|
+
Version.new(
|
34
|
+
number: title[VERSION_LINE_REGEXP, :version],
|
35
|
+
header: title,
|
36
|
+
body: nodes.map(&I::Kramdowns.method(:el2md)).join
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Changes
|
3
|
+
class Parser
|
4
|
+
extend I::Callable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def call(file)
|
8
|
+
parser_for(file).versions
|
9
|
+
end
|
10
|
+
|
11
|
+
def parser_for(file)
|
12
|
+
case file.basename
|
13
|
+
when /\.(md|markdown)$/i
|
14
|
+
MarkdownParser.new(file)
|
15
|
+
else
|
16
|
+
# Most of the time in Ruby-land, when no extension it is RDoc or RDoc-alike
|
17
|
+
RDocParser.new(file)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :file
|
23
|
+
|
24
|
+
def initialize(file)
|
25
|
+
@file = file
|
26
|
+
end
|
27
|
+
|
28
|
+
memoize def versions
|
29
|
+
fail NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
memoize def content
|
35
|
+
file.read
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require_relative 'markdown_parser'
|
42
|
+
require_relative 'rdoc_parser'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Changes
|
3
|
+
# FIXME: It kinda duplicates MarkdownParser for the _logic_, so should be unified somehow?
|
4
|
+
class RDocParser < Parser
|
5
|
+
def versions
|
6
|
+
nodes = I::RDocs.parts(content)
|
7
|
+
level = detect_version_level(nodes) # find level of headers which contain version
|
8
|
+
|
9
|
+
sections = sections(nodes, level)
|
10
|
+
.select { |title,| title.match?(VERSION_LINE_REGEXP) }
|
11
|
+
.map(&method(:make_version))
|
12
|
+
.sort_by(&:number) # it is internally converted to Gem::Version, so "1.12" is correctly > "1.2"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def sections(nodes, level)
|
18
|
+
nodes
|
19
|
+
.chunk { |n| n.is_a?(RDoc::Markup::Heading) && n.level == level } # chunk into sections by header
|
20
|
+
.drop_while { |header,| !header } # drop before first header
|
21
|
+
.map(&:last) # drop `true`, `false` flags after chunk
|
22
|
+
.each_slice(2) # join header with subsequent nodes
|
23
|
+
.map { |(h, *), nodes| [h.text, nodes] }
|
24
|
+
end
|
25
|
+
|
26
|
+
def detect_version_level(nodes)
|
27
|
+
nodes.grep(RDoc::Markup::Heading)
|
28
|
+
.select { |n| n.text.match?(VERSION_LINE_REGEXP) }
|
29
|
+
.map(&:level).min
|
30
|
+
end
|
31
|
+
|
32
|
+
def make_version((title, nodes))
|
33
|
+
# TODO: date, if known
|
34
|
+
Version.new(
|
35
|
+
number: title[VERSION_LINE_REGEXP, :version],
|
36
|
+
header: title,
|
37
|
+
body: nodes.map(&I::RDocs.method(:part2md)).join
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Changes
|
3
|
+
class ReleasesParser
|
4
|
+
extend I::Callable
|
5
|
+
|
6
|
+
def self.call(releases)
|
7
|
+
new(releases).versions
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :releases
|
11
|
+
|
12
|
+
def initialize(releases)
|
13
|
+
@releases = releases
|
14
|
+
end
|
15
|
+
|
16
|
+
def versions
|
17
|
+
releases.map { |tag_name:, name:, body:, **|
|
18
|
+
Version.new(
|
19
|
+
number: tag_name[VERSION_LINE_REGEXP, :version],
|
20
|
+
header: name.then.reject(&:empty?).first || tag_name,
|
21
|
+
body: body
|
22
|
+
)
|
23
|
+
}.sort_by(&:number)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Changes < Command
|
3
|
+
register description: 'Latest gem changes'
|
4
|
+
|
5
|
+
VERSION_REGEXP = '(v(er(sion)?)? ?)?(?<version>\d+\.\d+(\.\d+(\.\w+)?)?)'
|
6
|
+
VERSION_LINE_REGEXP = /(^#{VERSION_REGEXP}(\s|:|$)|\s#{VERSION_REGEXP}$)/i
|
7
|
+
|
8
|
+
CHANGELOG_NOT_FOUND = I::Pastel.red.bold("Can't find changelog to extract versions.")
|
9
|
+
|
10
|
+
class Version < Struct.new(:number, :header, :body, keyword_init: true)
|
11
|
+
def initialize(number:, header:, body:)
|
12
|
+
super(number: ::Gem::Version.new(number), header: header, body: body)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
super.merge(number: number.to_s)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
BundledSince = Struct.new(:version) do
|
21
|
+
def to_h
|
22
|
+
{description: "your bundled version: #{version}"}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
GlobalSince = Struct.new(:version) do
|
27
|
+
def to_h
|
28
|
+
{description: "your latest global version: #{version}"}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
SinceFirst = Struct.new(:version) do
|
33
|
+
def to_h
|
34
|
+
{description: "the first version"}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
TEMPLATE = Template.parse(<<~CHANGES)
|
39
|
+
_(since {{ since.description }})_
|
40
|
+
|
41
|
+
{% for version in versions %}
|
42
|
+
### {{ version.header }}
|
43
|
+
|
44
|
+
{{ version.body | md_header_shift:4 }}
|
45
|
+
|
46
|
+
{% endfor %}
|
47
|
+
|
48
|
+
CHANGES
|
49
|
+
|
50
|
+
def locals
|
51
|
+
{
|
52
|
+
since: since.to_h,
|
53
|
+
versions: select_versions.reverse.map(&:to_h)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def output
|
60
|
+
return CHANGELOG_NOT_FOUND unless versions
|
61
|
+
markdown super
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: allow to pass `since` as a parameter
|
65
|
+
def since
|
66
|
+
bundled_since || global_since || SinceFirst.new(versions.first.number)
|
67
|
+
end
|
68
|
+
|
69
|
+
def select_versions
|
70
|
+
return [] unless versions&.any?
|
71
|
+
res = versions.select { |v| v.number > since.version }
|
72
|
+
res = versions if res.empty? # if we are at the last version, show all historical changes
|
73
|
+
|
74
|
+
# TODO: If there are a lot of versions, we can do "intellectual selection", like
|
75
|
+
# two last minor + last major or something
|
76
|
+
|
77
|
+
res
|
78
|
+
end
|
79
|
+
|
80
|
+
def bundled_since
|
81
|
+
return unless gem.bundled.present?
|
82
|
+
gem.bundled.spec.version.then(&BundledSince.method(:new))
|
83
|
+
end
|
84
|
+
|
85
|
+
def global_since
|
86
|
+
gem.specs.first&.version&.then(&GlobalSince.method(:new))
|
87
|
+
end
|
88
|
+
|
89
|
+
memoize def versions
|
90
|
+
best_changelog(versions_from_releases, versions_from_file)
|
91
|
+
end
|
92
|
+
|
93
|
+
def versions_from_releases
|
94
|
+
gem.github&.releases&.then(&ReleasesParser)
|
95
|
+
end
|
96
|
+
|
97
|
+
def versions_from_file
|
98
|
+
# always take the freshest from GitHub, even when installed locally
|
99
|
+
gem.github&.changelog&.then(&Parser)
|
100
|
+
end
|
101
|
+
|
102
|
+
def best_changelog(*changelogs)
|
103
|
+
# in case they have both (releases & CHANGELOG.md)...
|
104
|
+
list = changelogs.compact.reject(&:empty?)
|
105
|
+
return list.first if list.size < 2
|
106
|
+
|
107
|
+
# Some gems have "old" changelog in file, and new in releases, or vice versa
|
108
|
+
if list.map { |*, last| last.number }.uniq.one?
|
109
|
+
# If both have the same latest version, return one having more text
|
110
|
+
list.max_by { |*, last| last.body.size }
|
111
|
+
else
|
112
|
+
# Return newer, if one of them are
|
113
|
+
list.max_by { |*, last| last.number }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
require_relative 'changes/parser'
|
120
|
+
require_relative 'changes/releases_parser'
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'tty/markdown'
|
2
|
+
require_relative 'tty-markdown_patch'
|
3
|
+
|
4
|
+
module WhatTheGem
|
5
|
+
class Command
|
6
|
+
Meta = Struct.new(:handle, :title, :description, keyword_init: true)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
memoize def registry
|
10
|
+
{}
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :meta
|
14
|
+
|
15
|
+
def register(title: name.split('::').last, handle: title.downcase, description:)
|
16
|
+
Command.registry[handle] = self
|
17
|
+
@meta = Meta.new(
|
18
|
+
handle: handle,
|
19
|
+
title: title,
|
20
|
+
description: description
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(handle)
|
25
|
+
Command.registry[handle]
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(*args)
|
29
|
+
new(*args).call
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# About info shortening: It is because minitest pastes their whole README
|
34
|
+
# there, including a quotations of how they are better than RSpec.
|
35
|
+
# (At the same time, RSpec's info reads "BDD for Ruby".)
|
36
|
+
|
37
|
+
# FIXME: It was > **{{ info.info | paragraphs:1 }}** but it looks weird due to tty-markdown
|
38
|
+
# bug: https://github.com/piotrmurach/tty-markdown/issues/11
|
39
|
+
HEADER_TEMPLATE = Template.parse(<<~HEADER)
|
40
|
+
# {{info.name}}
|
41
|
+
> {{info.info | paragraphs:1 | reflow }}
|
42
|
+
> ({{uris | join:", "}})
|
43
|
+
|
44
|
+
## {{title}}
|
45
|
+
|
46
|
+
|
47
|
+
HEADER
|
48
|
+
|
49
|
+
attr_reader :gem
|
50
|
+
|
51
|
+
def initialize(gem)
|
52
|
+
@gem = gem
|
53
|
+
end
|
54
|
+
|
55
|
+
def meta
|
56
|
+
self.class.meta
|
57
|
+
end
|
58
|
+
|
59
|
+
def call
|
60
|
+
puts full_output
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def full_output
|
66
|
+
gm = gem # otherwise next line breaks Sublime highlighting...
|
67
|
+
return %{Gem "#{gm.name}" is not registered at rubygems.org.} unless gem.exists?
|
68
|
+
header_locals.then(&HEADER_TEMPLATE).then(&method(:markdown)) + output
|
69
|
+
end
|
70
|
+
|
71
|
+
def output
|
72
|
+
locals.then(&template)
|
73
|
+
end
|
74
|
+
|
75
|
+
def header_locals
|
76
|
+
{
|
77
|
+
title: meta.title,
|
78
|
+
info: gem.rubygems.info,
|
79
|
+
uris: guess_uris(gem.rubygems.info),
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def markdown(text)
|
84
|
+
TTY::Markdown.parse(text)
|
85
|
+
end
|
86
|
+
|
87
|
+
def template
|
88
|
+
self.class::TEMPLATE
|
89
|
+
end
|
90
|
+
|
91
|
+
def guess_uris(info)
|
92
|
+
[
|
93
|
+
info[:source_code_uri],
|
94
|
+
info.values_at(:homepage_uri, :documentation_uri, :project_uri).compact.reject(&:empty?).first
|
95
|
+
]
|
96
|
+
.compact.reject(&:empty?).uniq { |u| u.chomp('/').sub('http:', 'https:') }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
%w[info usage changes stats help].each { |f| require_relative(f) }
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'backports/latest'
|
2
|
+
require 'memoist'
|
3
|
+
require 'pp'
|
4
|
+
require 'hm'
|
5
|
+
require 'pathname'
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
class Object
|
9
|
+
extend Memoist
|
10
|
+
alias then yield_self # will be so in Ruby 2.6
|
11
|
+
alias :_class :class
|
12
|
+
end
|
13
|
+
|
14
|
+
Module.include Forwardable
|
15
|
+
|
16
|
+
class Pathname
|
17
|
+
def glob(pattern) # exists in Ruby 2.5, but not in backports
|
18
|
+
Dir.glob(self./(pattern).to_s).map(&Pathname.method(:new))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Hm
|
23
|
+
class << self
|
24
|
+
alias call new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.to_proc
|
28
|
+
proc { |val| new(val) }
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
|
3
|
+
module WhatTheGem
|
4
|
+
class Gem
|
5
|
+
class Bundled
|
6
|
+
NoBundle = Struct.new(:name) do
|
7
|
+
def to_h
|
8
|
+
{type: 'nobundle'}
|
9
|
+
end
|
10
|
+
|
11
|
+
def present?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
NotBundled = Struct.new(:name) do
|
17
|
+
def to_h
|
18
|
+
{type: 'notbundled', name: name}
|
19
|
+
end
|
20
|
+
|
21
|
+
def present?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.fetch(name)
|
27
|
+
return NoBundle.new(name) unless File.exists?('Gemfile') && File.exists?('Gemfile.lock')
|
28
|
+
|
29
|
+
definition = Bundler::Definition.build('Gemfile', 'Gemfile.lock', nil)
|
30
|
+
spec = definition.locked_gems.specs.detect { |s| s.name == name } or return NotBundled.new(name)
|
31
|
+
spec.send(:__materialize__)
|
32
|
+
new(spec)
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :spec
|
36
|
+
|
37
|
+
def initialize(spec)
|
38
|
+
@spec = spec
|
39
|
+
end
|
40
|
+
|
41
|
+
def present?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
{
|
47
|
+
type: 'bundled',
|
48
|
+
name: spec.name,
|
49
|
+
version: spec.version.to_s,
|
50
|
+
dir: spec.gem_dir
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Gem
|
3
|
+
class GitHub
|
4
|
+
CHANGELOG_PATTERN = /^(history|changelog|changes)(\.\w+)?$/i
|
5
|
+
Path = Struct.new(:basename, :read)
|
6
|
+
|
7
|
+
class << self
|
8
|
+
memoize def octokit
|
9
|
+
if (token = ENV['GITHUB_ACCESS_TOKEN'])
|
10
|
+
Octokit::Client.new(access_token: token).tap { |client| client.user.login }
|
11
|
+
else
|
12
|
+
Octokit::Client.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :repo_id
|
18
|
+
|
19
|
+
def initialize(repo_id)
|
20
|
+
@repo_id = repo_id
|
21
|
+
end
|
22
|
+
|
23
|
+
memoize def repository
|
24
|
+
req(:repository)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias repo repository
|
28
|
+
|
29
|
+
memoize def last_commit
|
30
|
+
req(:commits, per_page: 1).first
|
31
|
+
end
|
32
|
+
|
33
|
+
memoize def open_issues
|
34
|
+
req(:issues, state: 'open', per_page: 50)
|
35
|
+
end
|
36
|
+
|
37
|
+
memoize def closed_issues
|
38
|
+
req(:issues, state: 'closed', per_page: 50)
|
39
|
+
end
|
40
|
+
|
41
|
+
memoize def releases
|
42
|
+
req(:releases)
|
43
|
+
end
|
44
|
+
|
45
|
+
def contents(path = '.')
|
46
|
+
req(:contents, path: path)
|
47
|
+
end
|
48
|
+
|
49
|
+
alias files contents
|
50
|
+
|
51
|
+
memoize def changelog
|
52
|
+
locate_file(CHANGELOG_PATTERN)
|
53
|
+
end
|
54
|
+
|
55
|
+
memoize def readme
|
56
|
+
locate_file(/^readme(\.\w+)?$/i)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def locate_file(pattern)
|
62
|
+
files.detect { |f| f.fetch(:name).match?(pattern) }
|
63
|
+
&.fetch(:path)
|
64
|
+
&.then { |path|
|
65
|
+
Path.new(path, contents(path).then(&method(:decode_content)))
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def req(method, *args)
|
70
|
+
octokit.public_send(method, repo_id, *args).then(&method(:sawyer_to_hashes))
|
71
|
+
end
|
72
|
+
|
73
|
+
def sawyer_to_hashes(val)
|
74
|
+
case
|
75
|
+
when val.respond_to?(:to_hash)
|
76
|
+
val.to_hash.transform_values(&method(:sawyer_to_hashes))
|
77
|
+
when val.respond_to?(:to_ary)
|
78
|
+
val.to_ary.map(&method(:sawyer_to_hashes))
|
79
|
+
else
|
80
|
+
val
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def decode_content(obj)
|
85
|
+
# TODO: encoding is specified as a part of the answer, could it be other than base64?
|
86
|
+
Base64.decode64(obj.fetch(:content)).force_encoding('UTF-8')
|
87
|
+
end
|
88
|
+
|
89
|
+
def octokit
|
90
|
+
self.class.octokit
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WhatTheGem
|
2
|
+
class Gem
|
3
|
+
class RubyGems
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
memoize def info
|
11
|
+
req(:info)
|
12
|
+
end
|
13
|
+
|
14
|
+
memoize def versions
|
15
|
+
req(:versions)
|
16
|
+
end
|
17
|
+
|
18
|
+
memoize def reverse_dependencies
|
19
|
+
req(:reverse_dependencies)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def req(method, *args)
|
25
|
+
::Gems.public_send(method, name, *args).then(&Hm).transform_keys(&:to_sym).to_h
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|