faf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9891acf05f5536961bacc63ba7eebf9d3e776c53e4ec4acbe12f282292514de4
4
+ data.tar.gz: ca5ae0e56664c71ce79266ef9a157425ae925eaaa43cd3aa0990fd6fc501a99f
5
+ SHA512:
6
+ metadata.gz: ced0850a182bb36a39d947cd482cf51df80bb698fcb001284170b9d0e685677c154de19b64acb82b86e1eacb66a5787217f869b79a96f1c6d468b3f98be0ca44
7
+ data.tar.gz: d3ded6c244bd2cf1f7fab2d0d27f477f2eeaefd0a1db89210177885590459079e9ca27f3c5ac2d4549838a4ea25102ee0d4ef174fa62c090c956337ef6f88851
@@ -0,0 +1,3 @@
1
+ ### 0.1.0 (2019/3/21)
2
+
3
+ * Initial public release - [@dblock](https://github.com/dblock).
@@ -0,0 +1,125 @@
1
+ # Contributing to Faf
2
+
3
+ This project is work of [many contributors](https://github.com/dblock/faf/graphs/contributors).
4
+
5
+ You're encouraged to submit [pull requests](https://github.com/dblock/faf/pulls), [propose features and discuss issues](https://github.com/dblock/faf/issues).
6
+
7
+ In the examples below, substitute your GitHub username for `contributor` in URLs.
8
+
9
+ ### Fork the Project
10
+
11
+ Fork the [project on GitHub](https://github.com/dblock/faf) and check out your copy.
12
+
13
+ ```
14
+ git clone https://github.com/contributor/faf.git
15
+ cd faf
16
+ git remote add upstream https://github.com/dblock/faf.git
17
+ ```
18
+
19
+ ### Bundle Install and Test
20
+
21
+ Ensure that you can build the project and run tests.
22
+
23
+ ```
24
+ bundle install
25
+ bundle exec rake
26
+ ```
27
+
28
+ ## Contribute Code
29
+
30
+ ### Create a Topic Branch
31
+
32
+ Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
33
+
34
+ ```
35
+ git checkout master
36
+ git pull upstream master
37
+ git checkout -b my-feature-branch
38
+ ```
39
+
40
+ ### Write Tests
41
+
42
+ Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add tests to [spec](spec).
43
+
44
+ We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
45
+
46
+ ### Write Code
47
+
48
+ Implement your feature or bug fix.
49
+
50
+ Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop). Run `bundle exec rubocop` and fix any style issues highlighted, auto-correct issues when possible with `bundle exec rubocop -a`. To silence generally ignored issues, including line lengths or code complexity metrics, run `bundle exec rubocop --auto-gen-config`.
51
+
52
+ Make sure that `bundle exec rake` completes without errors.
53
+
54
+ ### Write Documentation
55
+
56
+ Document any external behavior in the [README](README.md).
57
+
58
+ ### Update Changelog
59
+
60
+ Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Don't remove *Your contribution here*.
61
+
62
+ Make it look like every other line, including a link to the issue being fixed, your name and link to your Github account.
63
+
64
+ ### Commit Changes
65
+
66
+ Make sure git knows your name and email address:
67
+
68
+ ```
69
+ git config --global user.name "Your Name"
70
+ git config --global user.email "contributor@example.com"
71
+ ```
72
+
73
+ Writing good commit logs is important. A commit log should describe what changed and why.
74
+
75
+ ```
76
+ git add ...
77
+ git commit
78
+ ```
79
+
80
+ ### Push
81
+
82
+ ```
83
+ git push origin my-feature-branch
84
+ ```
85
+
86
+ ### Make a Pull Request
87
+
88
+ Go to https://github.com/contributor/faf and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days.
89
+
90
+ ### Update CHANGELOG Again
91
+
92
+ Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows.
93
+
94
+ ```
95
+ * [#123](https://github.com/dblock/faf/pull/123): Reticulated splines - [@contributor](https://github.com/contributor).
96
+ ```
97
+
98
+ Amend your previous commit and force push the changes.
99
+
100
+ ```
101
+ git commit --amend
102
+ git push origin my-feature-branch -f
103
+ ```
104
+
105
+ ### Rebase
106
+
107
+ If you've been working on a change for a while, rebase with upstream/master.
108
+
109
+ ```
110
+ git fetch upstream
111
+ git rebase upstream/master
112
+ git push origin my-feature-branch -f
113
+ ```
114
+
115
+ ### Check on Your Pull Request
116
+
117
+ Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
118
+
119
+ ### Be Patient
120
+
121
+ It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there!
122
+
123
+ ## Thank You
124
+
125
+ Please do know that we really appreciate and value your time and work. We love you, really.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Daniel Doubrovkine.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Faf
2
+ ===
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/faf.svg)](https://badge.fury.io/rb/faf)
5
+ [![Build Status](https://travis-ci.org/dblock/faf.svg)](https://travis-ci.org/dblock/faf)
6
+
7
+ Find Active (Github) Forks.
8
+
9
+ ## Usage
10
+
11
+ ```
12
+ gem install faf
13
+ ```
14
+
15
+ #### Show Github Forks
16
+
17
+ The `forks` command shows github forks.
18
+
19
+ ```bash
20
+ $ faf forks dblock/fue
21
+
22
+ https://github.com/zacklayton/fue (21 days and 7 hours ago)
23
+ https://github.com/lhmzhou/fue (25 days and 4 hours ago)
24
+ ```
25
+
26
+ Limit the number of forks with `--max`.
27
+
28
+ ```bash
29
+ $ faf forks --max=3 dblock/slack-gamebot
30
+
31
+ https://github.com/tarikstafford/slack-gamebot (18 days ago)
32
+ https://github.com/dersam/slack-gamebot (30 days and 4 hours ago)
33
+ https://github.com/ccadoret/slack-gamebot (50 days and 3 hours ago)
34
+ ```
35
+
36
+ #### Get Help
37
+
38
+ ```
39
+ faf help
40
+ ```
41
+
42
+ Displays additional options.
43
+
44
+ #### Access Tokens
45
+
46
+ Faf will prompt you for Github credentials and 2FA, if enabled.
47
+
48
+ ```
49
+ $ faf forks dblock/fue
50
+ Enter dblock's GitHub password (never stored): ******************
51
+ Enter GitHub 2FA code: ******
52
+ Token saved to keychain.
53
+ ```
54
+
55
+ The access token will be generated with `public_repo` scope and stored in the keychain. It can be later deleted from [here](https://github.com/settings/tokens). You can also skip the prompts and use a previously obtained token with `-t` or by setting the `GITHUB_ACCESS_TOKEN` environment variable.
56
+
57
+ See [Creating a Personal Access Token for the Command Line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) for more information about personal tokens.
58
+
59
+ ## Contributing
60
+
61
+ There are [a few feature requests and known issues](https://github.com/dblock/faf/issues). Please contribute! See [CONTRIBUTING](CONTRIBUTING.md).
62
+
63
+ ## Copyright and License
64
+
65
+ Copyright (c) 2019, Daniel Doubrovkine.
66
+
67
+ This project is licensed under the [MIT License](LICENSE.md).
data/bin/faf ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ require 'faf'
4
+
5
+ class App
6
+ extend GLI::App
7
+
8
+ program_desc 'Find active GitHub forks.'
9
+ version Faf::VERSION
10
+
11
+ switch %i[v verbose], desc: 'Produce verbose output.', default_value: false
12
+ flag %i[t token], desc: 'GitHub access token.', default_value: ENV['GITHUB_ACCESS_TOKEN']
13
+ flag %i[u username], desc: 'GitHub username.', default_value: Faf::Auth.instance.username
14
+
15
+ arguments :strict
16
+ subcommand_option_handling :normal
17
+
18
+ pre do |global_options, _command, options, _args|
19
+ options = global_options.dup
20
+ token = options.delete(:token) || Faf::Auth.instance.token
21
+ $verbose = global_options[:verbose]
22
+ puts "Using token '#{token}'" if $verbose
23
+ $connection = Faf::Connection.initialize!(token, options)
24
+ end
25
+
26
+ default_command :help
27
+
28
+ desc 'Show active GitHub forks.'
29
+ arg 'repo'
30
+ command :forks do |c|
31
+ c.flag %i[m max], desc: 'Maximum number of forks to show.'
32
+ c.action do |_global_options, options, args|
33
+ repo = args.first
34
+ puts "Exploring forks of '#{repo}' ..." if $verbose
35
+ github_repo = Faf::Repo.new(repo, options)
36
+ puts "Found #{github_repo.fork_count} forks ..." if $verbose
37
+ github_forks = github_repo.forks
38
+ github_forks = github_forks.take(options[:max].to_i) if options[:max]
39
+ github_forks.each do |f|
40
+ puts f
41
+ end
42
+ exit_now! nil, 0
43
+ end
44
+ end
45
+
46
+ on_error do |e|
47
+ warn "Error: #{e.message}"
48
+ warn ' ' + e.backtrace.join("\n ") if $verbose
49
+ false
50
+ end
51
+ end
52
+
53
+ exit App.run(ARGV)
@@ -0,0 +1,13 @@
1
+ require 'graphlient'
2
+ require 'github_api'
3
+ require 'open3'
4
+ require 'English'
5
+ require 'time_ago_in_words'
6
+
7
+ require 'faf/version'
8
+ require 'faf/connection'
9
+ require 'faf/repo'
10
+ require 'faf/forks'
11
+ require 'faf/shell'
12
+ require 'faf/security'
13
+ require 'faf/auth'
@@ -0,0 +1,108 @@
1
+ module Faf
2
+ class Auth
3
+ def self.instance
4
+ @instance ||= new
5
+ end
6
+
7
+ def token
8
+ stored_options = { username: username, server: 'github.com', label: 'faf' }
9
+ stored_token = Faf::Security.get(stored_options)
10
+ unless stored_token
11
+ stored_token = github_token
12
+ Faf::Security.store!(stored_options.merge(password: stored_token))
13
+ puts 'Token saved to keychain.'
14
+ end
15
+ stored_token
16
+ end
17
+
18
+ def username
19
+ @username ||= begin
20
+ username = get_git_username
21
+ username.chomp! if username
22
+ username = get_username if username.nil? || username.empty?
23
+ username
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def get_git_username
30
+ Faf::Shell.system!('git config github.user')
31
+ rescue RuntimeError
32
+ nil
33
+ end
34
+
35
+ def github_token(code = nil)
36
+ github(code).auth.create(scopes: ['public_repo'], note: note).token
37
+ rescue Github::Error::Unauthorized => e
38
+ case e.response_headers['X-GitHub-OTP']
39
+ when /required/ then
40
+ github_token(get_code)
41
+ else
42
+ raise e
43
+ end
44
+ rescue Github::Error::UnprocessableEntity => e
45
+ raise e, 'A faf token already exists! Please revoke all previously-generated faf personal access tokens at https://github.com/settings/tokens.'
46
+ end
47
+
48
+ def password
49
+ @password ||= get_password
50
+ end
51
+
52
+ def note
53
+ "Fui (https://github.com/dblock/faf) on #{Socket.gethostname}"
54
+ end
55
+
56
+ def github(code = nil)
57
+ Github.new do |config|
58
+ config.basic_auth = [username, password].join(':')
59
+ if code
60
+ config.connection_options = {
61
+ headers: {
62
+ 'X-GitHub-OTP' => code
63
+ }
64
+ }
65
+ end
66
+ end
67
+ end
68
+
69
+ def get_username
70
+ print 'Enter GitHub username: '
71
+ username = $stdin.gets
72
+ username.chomp! if username
73
+ username
74
+ rescue Interrupt => e
75
+ raise e, 'ctrl + c'
76
+ end
77
+
78
+ def get_password
79
+ print "Enter #{username}'s GitHub password (never stored): "
80
+ get_secure
81
+ end
82
+
83
+ def get_code
84
+ print 'Enter GitHub 2FA code: '
85
+ get_secure
86
+ end
87
+
88
+ def get_secure
89
+ current_tty = `stty -g`
90
+ system 'stty raw -echo -icanon isig' if $CHILD_STATUS.success?
91
+ input = ''
92
+ while (char = $stdin.getbyte) && !((char == 13) || (char == 10))
93
+ if (char == 127) || (char == 8)
94
+ input[-1, 1] = '' unless input.empty?
95
+ else
96
+ $stdout.write '*'
97
+ input << char.chr
98
+ end
99
+ end
100
+ print "\r\n"
101
+ input
102
+ rescue Interrupt => e
103
+ raise e, 'ctrl + c'
104
+ ensure
105
+ system "stty #{current_tty}" unless current_tty.empty?
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,42 @@
1
+ module Faf
2
+ class Connection
3
+ class << self
4
+ attr_reader :instance
5
+
6
+ def initialize!(token, options = {})
7
+ @instance ||= new(token, options)
8
+ end
9
+
10
+ def reset!
11
+ @instance = nil
12
+ end
13
+
14
+ def query(query, options = {})
15
+ raise 'Not Initialized' unless instance
16
+ instance.query(query, options)
17
+ end
18
+ end
19
+
20
+ attr_reader :token
21
+
22
+ def initialize(token, _options = {})
23
+ @token = token
24
+ end
25
+
26
+ def query(query, options)
27
+ graphql_client.query(query, options)
28
+ end
29
+
30
+ private
31
+
32
+ def graphql_client
33
+ @graphql_client ||= Graphlient::Client.new(
34
+ 'https://api.github.com/graphql',
35
+ headers: {
36
+ 'Authorization' => "Bearer #{token}",
37
+ 'Content-Type' => 'application/json'
38
+ }
39
+ )
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,61 @@
1
+ module Faf
2
+ class Forks
3
+ include Enumerable
4
+
5
+ attr_reader :repo
6
+
7
+ def initialize(repo, _options = {})
8
+ @repo = repo
9
+ end
10
+
11
+ def each
12
+ forks.each do |f|
13
+ yield(f)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def forks
20
+ @forks ||= get_forks
21
+ end
22
+
23
+ def get_forks(_options = {})
24
+ query = <<-GRAPHQL
25
+ query($owner: String!, $name: String!, $cursor: String) {
26
+ repository(owner: $owner, name: $name) {
27
+ forks(first: 100, after: $cursor, orderBy: { field: PUSHED_AT, direction: DESC }) {
28
+ edges {
29
+ cursor
30
+ node {
31
+ nameWithOwner
32
+ pushedAt
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ GRAPHQL
39
+
40
+ query_options = {
41
+ owner: repo.owner,
42
+ name: repo.name
43
+ }
44
+
45
+ forks = []
46
+
47
+ loop do
48
+ response = Faf::Connection.query(query, query_options)
49
+ edges = response.data.repository.forks.edges if response
50
+ break unless edges && edges.any?
51
+ edges.each do |edge|
52
+ query_options[:cursor] = edge.cursor
53
+ node = edge.node
54
+ forks << Faf::Repo.new(node.name_with_owner, pushed_at: Time.parse(node.pushed_at))
55
+ end
56
+ end
57
+
58
+ forks
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ module Faf
2
+ class Repo
3
+ attr_reader :owner
4
+ attr_reader :name
5
+ attr_reader :pushed_at
6
+
7
+ def initialize(url, options = {})
8
+ @owner, @name = url.split('/', 2)
9
+ @pushed_at = options[:pushed_at]
10
+ end
11
+
12
+ def forks
13
+ @forks ||= Faf::Forks.new(self)
14
+ end
15
+
16
+ def fork_count
17
+ repository.fork_count
18
+ end
19
+
20
+ def repository
21
+ data.repository
22
+ end
23
+
24
+ def url
25
+ "https://github.com/#{owner}/#{name}"
26
+ end
27
+
28
+ def to_s
29
+ "#{url} (#{pushed_at.ago_in_words})"
30
+ end
31
+
32
+ private
33
+
34
+ def data
35
+ @data ||= get_data.data
36
+ end
37
+
38
+ def get_data
39
+ query = <<-GRAPHQL
40
+ query($owner: String!, $name: String!) {
41
+ repository(owner: $owner, name: $name) {
42
+ name
43
+ forkCount
44
+ }
45
+ }
46
+ GRAPHQL
47
+
48
+ query_options = {
49
+ owner: owner,
50
+ name: name
51
+ }
52
+
53
+ Faf::Connection.query(query, query_options)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ module Faf
2
+ module Security
3
+ class << self
4
+ def store!(options)
5
+ Faf::Shell.system!(security('add', options))
6
+ end
7
+
8
+ def get(options)
9
+ Faf::Shell.system!(security('find', options))
10
+ rescue RuntimeError
11
+ nil
12
+ end
13
+
14
+ private
15
+
16
+ def security(command = nil, options = nil)
17
+ run = [security_path]
18
+ run << "#{command}-internet-password"
19
+ run << "-a #{options[:username]}"
20
+ run << "-s #{options[:server]}"
21
+ if command == 'add'
22
+ run << "-l #{options[:label]}"
23
+ run << '-U'
24
+ run << "-w #{options[:password]}" if options.key?(:password)
25
+ else
26
+ run << '-w'
27
+ end
28
+ run.join ' '
29
+ end
30
+
31
+ def security_path
32
+ @security_path ||= begin
33
+ `which security`.chomp
34
+ rescue StandardError
35
+ 'security'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ module Faf
2
+ module Shell
3
+ class << self
4
+ def system!(*cmd)
5
+ stdout, stderr, status = Open3.capture3(*cmd)
6
+ raise ["exit code #{status}", stderr].compact.join("\n") unless status.success?
7
+ stdout.slice!(0..-(1 + $INPUT_RECORD_SEPARATOR.size))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Faf
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Doubrovkine
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: github_api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.18.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.18.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: gli
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.17'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: graphlient
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: time_ago_in_words
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email: dblock@dblock.org
71
+ executables:
72
+ - faf
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - CONTRIBUTING.md
78
+ - LICENSE.md
79
+ - README.md
80
+ - bin/faf
81
+ - lib/faf.rb
82
+ - lib/faf/auth.rb
83
+ - lib/faf/connection.rb
84
+ - lib/faf/forks.rb
85
+ - lib/faf/repo.rb
86
+ - lib/faf/security.rb
87
+ - lib/faf/shell.rb
88
+ - lib/faf/version.rb
89
+ homepage: http://github.com/dblock/faf
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 1.3.6
107
+ requirements: []
108
+ rubygems_version: 3.0.1
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Find active GitHub forks.
112
+ test_files: []