whatthegem 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/whatthegem.svg)](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
|
+
![](https://github.com/zverok/whatthegem/blob/master/screenshots/info.png?raw=true)
|
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
|
+
![](https://github.com/zverok/whatthegem/blob/master/screenshots/usage.png?raw=true)
|
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
|
+
![](https://github.com/zverok/whatthegem/blob/master/screenshots/changes.png?raw=true)
|
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
|
+
![](https://github.com/zverok/whatthegem/blob/master/screenshots/stats.png?raw=true)
|
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
|