github-lister-core 0.1.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.
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,45 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'github-lister-core/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'github-lister-core'
8
+ spec.version = GithubListerCore::VERSION
9
+ spec.authors = ['Tim Gurney aka Wolf']
10
+ spec.email = ['wolf@tgwolf.com']
11
+
12
+ spec.summary = 'Provide a simple, fast and, clean way to extract repository information from GitHub.'
13
+ spec.description = 'The aim of the gem is to provide a simple, fast and, clean way to extra repository information from GitHub. It comes with a number of methods but it is intentionally limited to repository based information.'
14
+ spec.homepage = 'https://github.com/DevelopersToolbox/github-lister-core'
15
+ spec.license = 'MIT'
16
+
17
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/DevelopersToolbox/github-lister-core'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/DevelopersToolbox/github-lister-core/blob/master/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ # rubocop:disable Layout/ExtraSpacing, Layout/SpaceAroundOperators
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ # rubocop:enable Layout/ExtraSpacing, Layout/SpaceAroundOperators
30
+
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_development_dependency 'bundler', '~> 2'
36
+ spec.add_development_dependency 'octokit', '~> 4'
37
+ spec.add_development_dependency 'parallel', '~> 1.2'
38
+ spec.add_development_dependency 'rake', '~> 12'
39
+ spec.add_development_dependency 'reek', '~> 6'
40
+ spec.add_development_dependency 'rspec', '~> 3.0'
41
+ spec.add_development_dependency 'rubocop', '~> 1.11.0'
42
+
43
+ spec.add_runtime_dependency 'octokit', '~> 4'
44
+ spec.add_runtime_dependency 'parallel', '~> 1.2'
45
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # This code handles the creation of the github client with or without an auth token
3
+ #
4
+ # This class smells of :reek:UtilityFunction
5
+ class GithubListerCore
6
+ class << self
7
+ #
8
+ # Everything from here is private
9
+ #
10
+
11
+ private
12
+
13
+ #
14
+ # Initialise the client and set auto_paginate to true
15
+ #
16
+ def init_client(options = {})
17
+ token = get_option(options, :token)
18
+ client = if token
19
+ Octokit::Client.new(:access_token => token)
20
+ else
21
+ Octokit::Client.new
22
+ end
23
+ client.auto_paginate = true
24
+ client
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,76 @@
1
+ #
2
+ # Define our own errors
3
+ #
4
+ class GithubListerCore
5
+ #
6
+ # Catch all - something bad happened but we don't know what
7
+ #
8
+ class UnknownError < StandardError
9
+ def initialize
10
+ super('Something bad happen!')
11
+ end
12
+ end
13
+
14
+ #
15
+ # User supplied an invalid token (instead of a missing token)
16
+ #
17
+ class InvalidTokenError < StandardError
18
+ def initialize
19
+ super('Invalid Token')
20
+ end
21
+ end
22
+
23
+ #
24
+ # User didn't supply a token but one was expected
25
+ #
26
+ class MissingTokenError < StandardError
27
+ def initialize
28
+ super('Missing Token - Please refer to https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token')
29
+ end
30
+ end
31
+
32
+ #
33
+ # Github rate limited us!
34
+ #
35
+ class TooManyRequests < StandardError
36
+ def initialize
37
+ super('Too Many Requests')
38
+ end
39
+ end
40
+
41
+ #
42
+ # Generic 'not found' for users / orgs etc
43
+ #
44
+ class NotFoundError < StandardError
45
+ def initialize
46
+ super('Entity Not Found')
47
+ end
48
+ end
49
+
50
+ #
51
+ # Docs to go here
52
+ #
53
+ class MissingOrganisationError < StandardError
54
+ def initialize
55
+ super('org_name MUST be passed as an option')
56
+ end
57
+ end
58
+
59
+ #
60
+ # Docs to go here
61
+ #
62
+ class InvalidOptionsHashError < StandardError
63
+ def initialize
64
+ super('Options must be passed as a hash')
65
+ end
66
+ end
67
+
68
+ #
69
+ # Must be string or array!
70
+ #
71
+ class InvalidParameterError < StandardError
72
+ def initialize
73
+ super('Value must be a string or an array')
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # Add topics to each repository
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ #
13
+ # Extra the topics slug from the payload and return it
14
+ #
15
+ def languages_private(client, repo)
16
+ function_wrapper(client, 'languages', repo)
17
+ end
18
+
19
+ #
20
+ # Add topics to each repo
21
+ #
22
+ # This method smells of :reek:FeatureEnvy
23
+ def add_languages_private(client, repos)
24
+ (repo_list ||= []) << Parallel.each(repos, :in_threads => repos.count) { |repo| repo[:languages] = languages_internal(client, repo[:full_name]) }
25
+ repo_list.flatten
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ #
2
+ # Pull information specific to organisations (org repos are in repos.rb)
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ # rubocop:disable Metrics/MethodLength
13
+ def get_org_list(options)
14
+ org_param = get_option(options, [:org_name, :org])
15
+
16
+ raise MissingOrganisationError.new unless org_param
17
+
18
+ case org_param
19
+ when Array
20
+ orgs = org_param
21
+ when String
22
+ orgs = convert_to_array(org_param)
23
+ else
24
+ raise InvalidParameterError.new
25
+ end
26
+ orgs
27
+ end
28
+ # rubocop:enable Metrics/MethodLength
29
+
30
+ #
31
+ # Get a list of organisations that a user is a member of
32
+ #
33
+ # This method smells of :reek:DuplicateMethodCall
34
+
35
+ def org_repos_from_github(client, org)
36
+ function_wrapper(client, 'organization_repositories', org)
37
+ end
38
+
39
+ def org_repos_in_parallel(client, orgs)
40
+ (repos ||= []) << Parallel.map(orgs, :in_threads => orgs.count) { |org| org_repos_from_github(client, org) }
41
+ clean_from_parallel(repos, :full_name)
42
+ end
43
+
44
+ # Get the org list for a list of users
45
+ def org_membership_from_github(client, org)
46
+ function_wrapper(client, 'organizations', org)
47
+ end
48
+
49
+ def org_membership_in_parallel(client, users)
50
+ (orgs ||= []) << Parallel.map(users, :in_threads => users.count) { |user| org_membership_from_github(client, user) }
51
+ clean_from_parallel(orgs, :login)
52
+ end
53
+
54
+ def org_membership_private(client, users)
55
+ org_membership_in_parallel(client, users)
56
+ end
57
+
58
+ def org_membership_slugs_private(client, users)
59
+ org_membership_in_parallel(client, users).map { |hash| hash[:login] }.compact
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ #
2
+ # Octokit has a missing PREVIEW type
3
+ #
4
+ class GithubListerCore
5
+ #
6
+ # Missing from Octokit
7
+ #
8
+ PREVIEW_TYPES = {
9
+ :visibility => 'application/vnd.github.nebula-preview+json'
10
+ }.freeze
11
+ end
@@ -0,0 +1,53 @@
1
+ #
2
+ # This class
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ def latest_release_private(client, repo)
13
+ begin
14
+ release = function_wrapper(client, 'latest_release', repo)
15
+ # rubocop:disable Lint/SuppressedException
16
+ rescue NotFoundError => _exception
17
+ end
18
+ # rubocop:enable Lint/SuppressedException
19
+ release || []
20
+ end
21
+
22
+ #
23
+ # Add topics to each repo
24
+ #
25
+ # This method smells of :reek:FeatureEnvy
26
+ def add_latest_release(client, repos)
27
+ (repo_list ||= []) << Parallel.each(repos, :in_threads => repos.count) { |repo| repo[:latest_release] = latest_release_private(client, repo[:full_name]) }
28
+ repo_list.flatten
29
+ end
30
+
31
+ #
32
+ # All releases
33
+ #
34
+ def releases_private(client, repo)
35
+ begin
36
+ releases = function_wrapper(client, 'releases', repo)
37
+ # rubocop:disable Lint/SuppressedException
38
+ rescue NotFoundError => _exception
39
+ end
40
+ # rubocop:enable Lint/SuppressedException
41
+ releases || []
42
+ end
43
+
44
+ #
45
+ # Add topics to each repo
46
+ #
47
+ # This method smells of :reek:FeatureEnvy
48
+ def add_releases(client, repos)
49
+ (repo_list ||= []) << Parallel.each(repos, :in_threads => repos.count) { |repo| repo[:releases] = releases_private(client, repo[:full_name]) }
50
+ repo_list.flatten
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,92 @@
1
+ #
2
+ # Pull information specific to respositories
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ ########################################################################
13
+ # User Owned Repositories #
14
+ ########################################################################
15
+
16
+ def user_repos_from_github(client, user)
17
+ function_wrapper(client, 'repositories', user, { :type => 'owner', :accept => PREVIEW_TYPES[:visibility] })
18
+ end
19
+
20
+ def user_repos_in_parallel(client, users)
21
+ (repos ||= []) << Parallel.map(users, :in_threads => users.count) { |user| user_repos_from_github(client, user) }
22
+ clean_from_parallel(repos, :full_name)
23
+ end
24
+
25
+ def user_repos_private(client, options, users)
26
+ repos = user_repos_in_parallel(client, users)
27
+ add_additional_info(client, options, repos)
28
+ end
29
+
30
+ def user_repo_slugs_private(client, users)
31
+ repos = user_repos_in_parallel(client, users)
32
+ repos.map { |hash| hash[:full_name] }.compact
33
+ end
34
+
35
+ ########################################################################
36
+ # Organisation Owned Repositories #
37
+ ########################################################################
38
+
39
+ def org_repos_from_github(client, org)
40
+ function_wrapper(client, 'organization_repositories', org)
41
+ end
42
+
43
+ def org_repos_in_parallel(client, orgs)
44
+ (repos ||= []) << Parallel.map(orgs, :in_threads => orgs.count) { |org| org_repos_from_github(client, org) }
45
+ clean_from_parallel(repos, :full_name)
46
+ end
47
+
48
+ def org_repos_private(client, options, orgs)
49
+ repos = org_repos_in_parallel(client, orgs)
50
+ add_additional_info(client, options, repos)
51
+ end
52
+
53
+ def org_repo_slugs_private(client, orgs)
54
+ repos = org_repos_in_parallel(client, orgs)
55
+ repos.map { |hash| hash[:full_name] }.compact
56
+ end
57
+
58
+ ########################################################################
59
+ # Organisation Owned Repositories for ALL Organsations users are a #
60
+ # member of #
61
+ ########################################################################
62
+
63
+ def org_members_repos_private(client, options, users)
64
+ orgs = org_membership_slugs_private(client, users)
65
+ repos = org_repos_in_parallel(client, orgs)
66
+ add_additional_info(client, options, repos)
67
+ end
68
+
69
+ def org_members_repo_slugs_private(client, users)
70
+ orgs = org_membership_slugs_private(client, users)
71
+ repos = org_repos_in_parallel(client, orgs)
72
+ repos.map { |hash| hash[:full_name] }.compact
73
+ end
74
+
75
+ #
76
+ # Generate a slub list of repos for all organisations that a user is a member of
77
+ #
78
+ def all_repos_private(client, options, users)
79
+ repos = user_repos_private(client, options, users) + org_repos_for_user_private(client, options, users)
80
+ repos.flatten.sort_by { |repo| repo[:full_name].downcase }
81
+ end
82
+
83
+ #
84
+ # Get a list of ALL repos that a user has access to (This is both personal and via organisations)
85
+ # If the user is authenticated and a member of the org it will list private + public
86
+ #
87
+ def all_repo_slugs_private(client, users)
88
+ repos = user_repo_slugs_private(client, users) + org_members_repo_slugs_private(client, users)
89
+ repos.flatten.sort
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,29 @@
1
+ #
2
+ # Add topics to each repository
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ #
13
+ # Extra the topics slug from the payload and return it
14
+ #
15
+ def topics_private(client, repo)
16
+ topics = function_wrapper(client, 'topics', repo, { :accept => Octokit::Preview::PREVIEW_TYPES[:topics] })
17
+ topics[:names]
18
+ end
19
+
20
+ #
21
+ # Add topics to each repo
22
+ #
23
+ # This method smells of :reek:FeatureEnvy
24
+ def add_topics_private(client, repos)
25
+ (repo_list ||= []) << Parallel.each(repos, :in_threads => repos.count) { |repo| repo[:topics] = topics_private(client, repo[:full_name]) }
26
+ repo_list.flatten
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ #
2
+ # Handle anything specific to the user
3
+ #
4
+ # This class smells of :reek:InstanceVariableAssumption
5
+ class GithubListerCore
6
+ class << self
7
+ #
8
+ # Everything below here is private
9
+ #
10
+
11
+ private
12
+
13
+ def authed_user_in_array(users, authed)
14
+ users.select { |user| user.downcase == authed.login.downcase } != []
15
+ end
16
+
17
+ def process_user_list(user_list, authed)
18
+ user_list = convert_to_array(user_list)
19
+
20
+ return user_list unless authed
21
+
22
+ if authed_user_in_array(user_list, authed)
23
+ user_list.delete_if { |user| user.downcase == authed.login.downcase }
24
+ user_list.append(authed)
25
+ end
26
+ user_list
27
+ end
28
+
29
+ #
30
+ # Users can be a string (comma separated) or an array nothing else
31
+ #
32
+ def get_user_list(client, options)
33
+ user_list = get_option(options, [:user, :username])
34
+ authed = client.user if client.user_authenticated?
35
+
36
+ return convert_to_array(authed) unless user_list
37
+
38
+ case user_list
39
+ when Array, String
40
+ process_user_list(user_list, authed)
41
+ else
42
+ raise InvalidParameterError.new
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ #
2
+ # Handle anything specific to the user
3
+ #
4
+ class GithubListerCore
5
+ class << self
6
+ #
7
+ # Everything below here is private
8
+ #
9
+
10
+ private
11
+
12
+ def clean_from_parallel(item, sorted = nil)
13
+ return [] if item.nil?
14
+
15
+ item = item.flatten.map(&:to_h).uniq
16
+ item = item.sort_by { |repo| repo[sorted].downcase } if sorted
17
+ item
18
+ end
19
+
20
+ def convert_to_array(item)
21
+ case item
22
+ when Sawyer::Resource
23
+ [].append(item)
24
+ when Array
25
+ item
26
+ when String
27
+ item.split(',').map(&:strip).uniq.sort
28
+ else
29
+ []
30
+ end
31
+ end
32
+
33
+ def validate_options(options)
34
+ raise InvalidOptionsHashError.new unless options.is_a?(Hash)
35
+ end
36
+
37
+ def look_for_option_from_array(options, names)
38
+ names.each { |name| return options[name] if options.key?(name) }
39
+ nil
40
+ end
41
+
42
+ def get_option(options, names)
43
+ return nil unless options
44
+ return nil unless options.is_a?(Hash)
45
+
46
+ case names
47
+ when Array
48
+ value = look_for_option_from_array(options, names)
49
+ when String, Symbol
50
+ value = options[names] if options.key?(names)
51
+ end
52
+ value || nil
53
+ end
54
+
55
+ def flag_set?(options, option)
56
+ value = get_option(options, option)
57
+
58
+ return false unless value
59
+
60
+ return false unless value == true
61
+
62
+ true
63
+ end
64
+
65
+ def add_additional_info(client, options, repos)
66
+ return repos if flag_set?(options, :use_slugs)
67
+
68
+ repos = add_topics_private(client, repos) if flag_set?(options, :add_topics)
69
+ repos = add_latest_release_private(client, repos) if flag_set?(options, :add_latest_release)
70
+ repos = add_releases_private(client, repos) if flag_set?(options, :add_releases)
71
+ repos = add_languages_private(client, repos) if flag_set?(options, :add_languages)
72
+ repos
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ class GithubListerCore
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,32 @@
1
+ #
2
+ # This class store the main processing wrapper which talks to the API and handles all the exceptions
3
+ #
4
+ # This class smells of :reek:InstanceVariableAssumption
5
+ class GithubListerCore
6
+ class << self
7
+ #
8
+ # Everything below here is private
9
+ #
10
+
11
+ private
12
+
13
+ # rubocop:disable Metrics/MethodLength
14
+ def function_wrapper(client, function, *param)
15
+ begin
16
+ results = client.send(function, *param)
17
+ rescue Octokit::Unauthorized
18
+ raise InvalidTokenError.new if client.user_authenticated?
19
+
20
+ raise MissingTokenError.new
21
+ rescue Octokit::NotFound
22
+ raise NotFoundError.new
23
+ rescue Octokit::TooManyRequests
24
+ raise TooManyRequests.new
25
+ rescue StandardError
26
+ raise UnknownError.new
27
+ end
28
+ results || []
29
+ end
30
+ end
31
+ # rubocop:enable Metrics/MethodLength
32
+ end