usedby 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: []