usedby 0.4.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51866979c0e578e1c2c4bda5c7f61e79cd9b6bb0a5366eaaf2c66f474b00c6ea
4
+ data.tar.gz: 54e3aa2e5a415b570ead51e7c004b5a37683f76360c8d084874f0b05812281c7
5
+ SHA512:
6
+ metadata.gz: da372934168ed4cf0fb96e648c7735631c0068652516fa08142a8666f20be2f57f9a0b35861aec240b6a2b2ee29cc35b5387b3fa9c4a9f152ac2375beadb7b04
7
+ data.tar.gz: 5ad1483db112e0b5cbf819f095de455dae8f90105c9a93fc85fcaf4d2c5bec296f9a9291519274ed03c54eee80e9dc878575961b638bb4a01288fe012442d8d7
data/CHANGES.md ADDED
@@ -0,0 +1,21 @@
1
+ # Change Log
2
+
3
+ ## 0.3.0 (2018/07/20)
4
+
5
+ **Added**
6
+
7
+ * Don't include archived repositories.
8
+
9
+
10
+ ## 0.2.0 (2018/07/03)
11
+
12
+ **Added**
13
+
14
+ * Require `GITHUB_ORGANIZATION` to be provided on the command line.
15
+
16
+ ## 0.1.0 (2018/07/03)
17
+
18
+ **Added**
19
+
20
+ * Provide initial version of the `organization_gem_dependencies` command line
21
+ tool.
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2018, AppFolio, Inc.
2
+ Copyright (c) 2018, Bryce Boe
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # usedby
2
+
3
+ Figure out where your gems are actually being used!
4
+ Similar to GitHub's "Used By" feature, but for private repos.
5
+
6
+ This gem installs a command line utility `usedby`, that
7
+ outputs a json file with a reverse dependency tree.
8
+
9
+ This acts more or less like GitHub's "Used By" feature.
10
+ It currently uses GitHub's code search API which has a few limitations:
11
+ https://help.github.com/en/github/searching-for-information-on-github/searching-code
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ gem install usedby
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```sh
22
+ usedby GITHUB_ORGANIZATION [--direct] [--gems GEM1,GEM2,GEM3]
23
+ ```
24
+
25
+ You will be securely prompted for a [GitHub Personal Access Token](https://github.com/settings/tokens).
26
+
27
+ For example, running `usedby rails --direct --gems railties,rake` produces output
28
+ like the following:
29
+
30
+ ```json
31
+ {
32
+ "railties": {
33
+ "4.0.0.beta": [
34
+ "routing_concerns/Gemfile.lock"
35
+ ],
36
+ "4.0.0": [
37
+ "prototype-rails/Gemfile.lock"
38
+ ],
39
+ "4.2.1": [
40
+ "rails-perftest/Gemfile.lock"
41
+ ],
42
+ "4.2.10": [
43
+ "rails-docs-server/test/fixtures/releases/v4.2.10/Gemfile.lock"
44
+ ],
45
+ "5.1.1": [
46
+ "actioncable-examples/Gemfile.lock"
47
+ ],
48
+ "5.2.1": [
49
+ "rails_fast_attributes/Gemfile.lock"
50
+ ],
51
+ "5.2.2": [
52
+ "globalid/Gemfile.lock"
53
+ ],
54
+ "6.0.1": [
55
+ "webpacker/Gemfile.lock"
56
+ ],
57
+ "6.0.2.1": [
58
+ "rails-contributors/Gemfile.lock"
59
+ ],
60
+ "6.1.0.alpha": [
61
+ "rails/Gemfile.lock"
62
+ ]
63
+ },
64
+ "rake": {
65
+ "0.9.2.2": [
66
+ "commands/Gemfile.lock",
67
+ "etagger/Gemfile.lock",
68
+ "routing_concerns/Gemfile.lock"
69
+ ],
70
+ "10.1.0": [
71
+ "prototype-rails/Gemfile.lock"
72
+ ],
73
+ "10.4.2": [
74
+ "jquery-ujs/Gemfile.lock",
75
+ "rails-perftest/Gemfile.lock"
76
+ ],
77
+ "10.5.0": [
78
+ "rails_fast_attributes/Gemfile.lock",
79
+ "record_tag_helper/Gemfile.lock"
80
+ ],
81
+ "12.0.0": [
82
+ "actioncable-examples/Gemfile.lock",
83
+ "rails-docs-server/test/fixtures/releases/v4.2.10/Gemfile.lock",
84
+ "rails-dom-testing/Gemfile.lock"
85
+ ],
86
+ "12.3.2": [
87
+ "globalid/Gemfile.lock"
88
+ ],
89
+ "13.0.0": [
90
+ "webpacker/Gemfile.lock"
91
+ ],
92
+ "13.0.1": [
93
+ "rails-contributors/Gemfile.lock",
94
+ "rails/Gemfile.lock"
95
+ ]
96
+ }
97
+ }
98
+
99
+ ```
data/bin/usedby ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'usedby'
5
+ exit Usedby::Cli.new.run
data/lib/usedby.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'usedby/cli'
4
+ require 'usedby/version'
data/lib/usedby/cli.rb ADDED
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'io/console'
5
+ require 'json'
6
+ require 'optparse'
7
+
8
+ require 'octokit'
9
+
10
+ require 'usedby/version_ranges_intersection'
11
+
12
+ module Usedby
13
+ # Define the command line interface.
14
+ class Cli
15
+ using VersionRangesIntersection
16
+
17
+ GEMFILE_LOCK_SEARCH_TERM = 'org:%s filename:Gemfile.lock'
18
+ GEMSPEC_SEARCH_TERM = 'org:%s extension:gemspec'
19
+ USAGE = <<~USAGE
20
+ Usage: usedby [options] GITHUB_ORGANIZATION
21
+ USAGE
22
+
23
+
24
+ def run
25
+ parse_options
26
+ if ARGV.size != 1
27
+ STDERR.puts USAGE
28
+ return 1
29
+ end
30
+ github_organization = ARGV[0]
31
+
32
+ access_token = ENV['GITHUB_ACCESS_TOKEN'] || \
33
+ STDIN.getpass('GitHub Personal Access Token: ')
34
+ github = Octokit::Client.new(access_token: access_token)
35
+
36
+ gems = {}
37
+
38
+ remote_search(github, github_organization, GEMFILE_LOCK_SEARCH_TERM) do |gemfile_lock|
39
+ content = remote_file(github, gemfile_lock)
40
+ next unless content
41
+ content = Bundler::LockfileParser.new(content)
42
+ merge!(gems, process_gemfile(content, "#{gemfile_lock.repository.name}/#{gemfile_lock.path}"))
43
+ end
44
+
45
+ remote_search(github, github_organization, GEMSPEC_SEARCH_TERM) do |gemspec|
46
+ content = remote_file(github, gemspec)
47
+ next unless content
48
+ merge!(gems, process_gemspec(content, "#{gemspec.repository.name}/#{gemspec.path}"))
49
+ end
50
+
51
+ output gems
52
+
53
+ 0
54
+ end
55
+
56
+ private
57
+
58
+ def archived_repositories(github, organization)
59
+ github.organization_repositories(organization)
60
+ last_response = github.last_response
61
+
62
+ repositories = []
63
+ last_response.data.each do |repository|
64
+ repositories << repository.name if repository.archived
65
+ end
66
+ until last_response.rels[:next].nil?
67
+ sleep_time = 0
68
+ begin
69
+ last_response = last_response.rels[:next].get
70
+ rescue StandardError
71
+ sleep_time += 1
72
+ STDERR.puts "Sleeping #{sleep_time} seconds"
73
+ sleep(sleep_time)
74
+ retry
75
+ end
76
+ last_response.data.each do |repository|
77
+ repositories << repository.name if repository.archived
78
+ end
79
+ end
80
+ repositories
81
+ end
82
+
83
+ def build_ignore_paths(ignored_paths, file)
84
+ File.open(file).each do |line|
85
+ cleaned = line.strip
86
+ ignored_paths << cleaned if cleaned != ''
87
+ end
88
+ rescue Errno::ENOENT, Errno::EISDIR
89
+ STDERR.puts "No such file #{file}"
90
+ exit 1
91
+ end
92
+
93
+ def filtered?(gemfile_path)
94
+ @options[:ignore_paths].each do |ignore_path|
95
+ return true if gemfile_path.start_with?(ignore_path)
96
+ end
97
+ false
98
+ end
99
+
100
+ def remote_search(github, organization, search_term)
101
+ archived = archived_repositories(github, organization)
102
+ github.search_code(search_term % organization, per_page: 1000)
103
+ last_response = github.last_response
104
+
105
+ matches = []
106
+ last_response.data.items.each do |match|
107
+ matches << match unless archived.include? match.repository.name
108
+ end
109
+ until last_response.rels[:next].nil?
110
+ sleep_time = 0
111
+ begin
112
+ last_response = last_response.rels[:next].get
113
+ rescue StandardError
114
+ sleep_time += 1
115
+ STDERR.puts "Sleeping #{sleep_time} seconds"
116
+ sleep(sleep_time)
117
+ retry
118
+ end
119
+ last_response.data.items.each do |match|
120
+ matches << match unless archived.include? match.repository.name
121
+ end
122
+ end
123
+
124
+ matches.sort_by(&:html_url).each do |match|
125
+ yield match
126
+ end
127
+ end
128
+
129
+ def remote_file(github, file)
130
+ github_path = "#{file.repository.name}/#{file.path}"
131
+ if filtered?(github_path)
132
+ STDERR.puts "Skipping #{github_path}"
133
+ return
134
+ end
135
+ STDERR.puts "Processing #{github_path}"
136
+ sleep_time = 0
137
+ begin
138
+ content = Base64.decode64(github.get(file.url).content)
139
+ rescue StandardError
140
+ sleep_time += 1
141
+ STDERR.puts "Sleeping #{sleep_time} seconds"
142
+ sleep(sleep_time)
143
+ retry
144
+ end
145
+ content
146
+ end
147
+
148
+ def merge!(base, additions)
149
+ additions.each do |gem, versions|
150
+ if base.include? gem
151
+ base_versions = base[gem]
152
+ versions.each do |version, projects|
153
+ if base_versions.include? version
154
+ base_versions[version].concat(projects)
155
+ else
156
+ base_versions[version] = projects
157
+ end
158
+ end
159
+ else
160
+ base[gem] = versions
161
+ end
162
+ end
163
+ end
164
+
165
+ def output(gems)
166
+ sorted_gems = {}
167
+ gems.sort.each do |gem, versions|
168
+ sorted_gems[gem] = {}
169
+ versions.sort.each do |version, projects|
170
+ sorted_gems[gem][version_ranges_to_s(version)] = projects.sort
171
+ end
172
+ end
173
+ puts JSON.pretty_generate(sorted_gems)
174
+ end
175
+
176
+ def parse_options
177
+ @options = { direct: false, ignore_paths: [] }
178
+ OptionParser.new do |config|
179
+ config.banner = USAGE
180
+ config.on('-d', '--direct',
181
+ 'Consider only direct dependencies.') do |direct|
182
+ @options[:direct] = direct
183
+ end
184
+ config.on('-i', '--ignore-file [FILEPATH]',
185
+ 'Ignore projects included in file.') do |ignore_file|
186
+
187
+ build_ignore_paths(@options[:ignore_paths], ignore_file)
188
+ end
189
+ config.on('-g', '--gems [GEM1,GEM2,GEM3]',
190
+ 'Consider only given gems.') do |gems|
191
+
192
+ @options[:gems] = gems.split(',')
193
+ end
194
+ config.version = Usedby::VERSION
195
+ end.parse!
196
+ end
197
+
198
+ def process_gemfile(gemfile, project)
199
+ dependencies = gemfile.dependencies.map { |dependency, _, _| dependency }
200
+ gems = {}
201
+
202
+ gemfile.specs.each do |spec|
203
+ next if @options[:direct] && !dependencies.include?(spec.name)
204
+ next if @options[:gems] && !@options[:gems].include?(spec.name)
205
+ spec_version_ranges = Bundler::VersionRanges.for(Gem::Requirement.new(spec.version))
206
+ gems[spec.name] = {}
207
+ gems[spec.name][spec_version_ranges] = [project]
208
+ end
209
+ gems
210
+ end
211
+
212
+ # Process dependencies in gemspec according to:
213
+ # https://guides.rubygems.org/specification-reference/
214
+ # Sample supported formats:
215
+ # s.add_dependency(%q<rspec>.freeze, ["~> 3.2"])
216
+ # spec.add_runtime_dependency "multi_json", "~>1.12", ">=1.12.0"
217
+ # Sample unsupported formats:
218
+ # s.add_development_dependency "rake", "~> 10.5" if on_less_than_1_9_3?
219
+ # s.add_dependency 'sunspot', Sunspot::VERSION
220
+ def process_gemspec(content, project)
221
+ gems = {}
222
+ dummy_spec = Gem::Specification.new
223
+ content.each_line do |line|
224
+ if line =~ /^\s*(\w+)\.add_(development_dependency|runtime_dependency|dependency)\b/
225
+ spec_name = $1
226
+ begin
227
+ eval line.sub(spec_name, 'dummy_spec')
228
+ rescue => e
229
+ $stderr.puts e
230
+ next
231
+ end
232
+ dep = dummy_spec.dependencies.last
233
+ gem_name = dep.name
234
+ next if @options[:gems] && !@options[:gems].include?(gem_name)
235
+ gem_version_ranges = Bundler::VersionRanges.for(dep.requirement)
236
+ gems[gem_name] = {}
237
+ gems[gem_name][gem_version_ranges] = [project]
238
+ end
239
+ end
240
+ gems
241
+ end
242
+
243
+ # Uses mathematical notation for ranges
244
+ # The unbounded range is [0, ∞)
245
+ def version_ranges_to_s(gem_version_ranges)
246
+ if !gem_version_ranges.kind_of?(Array) || gem_version_ranges.size != 2
247
+ $stderr.puts "Unknown format for version ranges: #{gem_version_ranges}"
248
+ return ""
249
+ end
250
+
251
+ # base case: a specific version
252
+ if gem_version_ranges[0].size == 1
253
+ range = gem_version_ranges[0][0]
254
+ if range.left.version == range.right.version
255
+ return range.left.version.to_s
256
+ end
257
+ end
258
+
259
+ gem_version_ranges[0] = Bundler::VersionRanges::ReqR.reduce(gem_version_ranges[0])
260
+
261
+ arr = []
262
+ gem_version_ranges[0].each do |reqr|
263
+ range_begin = reqr.left.inclusive ? "[" : "("
264
+ range_end = reqr.right.inclusive ? "]" : ")"
265
+ arr << "#{range_begin}#{reqr.left.version.to_s}, #{reqr.right.version.to_s}#{range_end}"
266
+ end
267
+ gem_version_ranges[1].each do |neq|
268
+ # special case: exclude specific version
269
+ arr << "!= #{neq.version.to_s}"
270
+ end
271
+ arr.join(", ")
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usedby
4
+ VERSION = '0.4.0'
5
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VersionRangesIntersection
4
+ refine Bundler::VersionRanges::ReqR do
5
+ # Checks whether two ranges overlap
6
+ def intersect?(other)
7
+ case self <=> other
8
+ when -1
9
+ return (right <=> other.left) == 1
10
+ when 1
11
+ return (other.right <=> left) == 1
12
+ end
13
+
14
+ true
15
+ end
16
+
17
+ # Returns a new range which is the intersection, or else nil
18
+ def intersection(other)
19
+ return nil unless intersect?(other)
20
+
21
+ inter = clone
22
+ case inter <=> other
23
+ when -1
24
+ inter.left = other.left
25
+ when 1
26
+ inter.right = other.right
27
+ end
28
+
29
+ inter
30
+ end
31
+ end
32
+
33
+ # Class methods need to be refined on the singleton_class
34
+ refine Bundler::VersionRanges::ReqR.singleton_class do
35
+ def reduce(reqrs = [])
36
+ reduced_reqrs = reqrs.clone
37
+ i = 0
38
+ while i < reduced_reqrs.size - 1
39
+ intersecting_range = reduced_reqrs[i].intersection(reduced_reqrs[i + 1])
40
+ if intersecting_range
41
+ reduced_reqrs[i + 1] = intersecting_range
42
+ reduced_reqrs.delete_at(i)
43
+ else
44
+ i += 1
45
+ end
46
+ end
47
+
48
+ reduced_reqrs
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: usedby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - AppFolio, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: octokit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ description: 'usedby is a command line tool to discover all dependents of ruby gems
56
+ across a github organization.
57
+
58
+ '
59
+ email: opensource@appfolio.com
60
+ executables:
61
+ - usedby
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - CHANGES.md
66
+ - LICENSE.txt
67
+ - README.md
68
+ - bin/usedby
69
+ - lib/usedby.rb
70
+ - lib/usedby/cli.rb
71
+ - lib/usedby/version.rb
72
+ - lib/usedby/version_ranges_intersection.rb
73
+ homepage: https://github.com/appfolio/usedby
74
+ licenses:
75
+ - BSD-2-Clause
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.1.2
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Discover all dependents of ruby gems across a github organization.
96
+ test_files: []