hubkit 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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +32 -0
- data/Gemfile +17 -0
- data/LICENSE +19 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/hubkit.gemspec +34 -0
- data/lib/hubkit.rb +33 -0
- data/lib/hubkit/chainable_collection.rb +106 -0
- data/lib/hubkit/configuration.rb +51 -0
- data/lib/hubkit/cooldowner.rb +20 -0
- data/lib/hubkit/event_collection.rb +58 -0
- data/lib/hubkit/event_paginator.rb +36 -0
- data/lib/hubkit/issue.rb +185 -0
- data/lib/hubkit/issue_collection.rb +101 -0
- data/lib/hubkit/issue_paginator.rb +23 -0
- data/lib/hubkit/logger.rb +62 -0
- data/lib/hubkit/paginator.rb +28 -0
- data/lib/hubkit/repo.rb +113 -0
- data/lib/hubkit/repo_collection.rb +14 -0
- data/lib/hubkit/repo_paginator.rb +17 -0
- data/lib/hubkit/version.rb +4 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 645c28ca63135ba2eb911fce4e0f759db34be764
|
4
|
+
data.tar.gz: db3abc9ba844450b14555e9cb743989d21896b03
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b63ffe491f25e4c10ebed01fbc6284e37f1a9f5ac99e1d6af79d08342b2a237362888623641259b1d1ee89e854eed3fa19b5789b245aedd4fdabc6871f91d469
|
7
|
+
data.tar.gz: 5859fd0c38ae794734299634298dfe1a8607545eee5d162f4544463488655c019a929cba670429a0688e2b8dca564e0159233f0647095d8033f7cdc33e069cf4
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at robert@revelry.co. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Setup for Development
|
2
|
+
|
3
|
+
```
|
4
|
+
bundle install
|
5
|
+
```
|
6
|
+
|
7
|
+
should be sufficient to install dependencies.
|
8
|
+
|
9
|
+
You will a `.env` file with your API keys to record new tests against the
|
10
|
+
API. Your API key will be omitted from the test cassettes by VCR. The format for
|
11
|
+
`.env` is:
|
12
|
+
|
13
|
+
```
|
14
|
+
GITHUB_TOKEN=YOUR-API-TOKEN
|
15
|
+
```
|
16
|
+
|
17
|
+
# Submitting Changes
|
18
|
+
|
19
|
+
1. Fork the repository.
|
20
|
+
2. Set up the gem per the instructions above and ensure `bundle exec rake spec`
|
21
|
+
runs cleanly.
|
22
|
+
3. Create a topic branch.
|
23
|
+
4. Add specs for your unimplemented feature or bug fix.
|
24
|
+
5. Run `bundle exec rake spec`. If your specs pass, return to step 4.
|
25
|
+
6. Implement your feature or bug fix.
|
26
|
+
7. Re-run `bundle exec rake spec`. If your specs fail, return to step 6.
|
27
|
+
8. Open coverage/index.html. If your changes are not completely covered by the
|
28
|
+
test suite, return to Step 4.
|
29
|
+
9. Thoroughly document and comment your code.
|
30
|
+
10. Run `bundle exec rake doc:yard` and make sure your changes are documented.
|
31
|
+
11. Add, commit, and push your changes.
|
32
|
+
12. Submit a pull request.
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in hubkit.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem 'byebug'
|
8
|
+
# rubocop:disable Style/ExtraSpacing
|
9
|
+
gem 'cucumber', '~> 2.1'
|
10
|
+
gem 'dotenv'
|
11
|
+
gem 'rspec', '~> 3.5.0'
|
12
|
+
gem 'rspec-collection_matchers'
|
13
|
+
gem 'simplecov', '~> 0.11.2'
|
14
|
+
gem 'vcr', '~> 2.6'
|
15
|
+
gem 'webmock', '~> 1.17.3'
|
16
|
+
gem 'yard', '~> 0.8.7'
|
17
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2017 Robert Prehn
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Hubkit
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hubkit`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'hubkit'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install hubkit
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/prehnRA/hubkit. Check out [CONTRIBUTING.md](https://github.com/prehnRA/hubkit/blob/master/CONTRIBUTING.md) for more info.
|
34
|
+
|
35
|
+
Everyone is welcome to participate in the project. We expect contributors to
|
36
|
+
adhere the Contributor Covenant Code of Conduct (see [CODE_OF_CONDUCT.md](https://github.com/prehnRA/hubkit/blob/master/CODE_OF_CONDUCT.md)).
|
37
|
+
|
38
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "hubkit"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/hubkit.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hubkit/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hubkit"
|
8
|
+
spec.version = Hubkit::VERSION
|
9
|
+
spec.authors = ["Robert Prehn"]
|
10
|
+
spec.email = ["robert@revelry.co"]
|
11
|
+
spec.licenses = ['MIT']
|
12
|
+
|
13
|
+
spec.summary = "Higher level abstractions for querying the github API"
|
14
|
+
spec.description = <<-DESC
|
15
|
+
Hubkit provides methods for querying the github API at a higher level than
|
16
|
+
making individual API calls. Think of it like an ORM for the github API.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
spec.homepage = "https://github.com/prehnRA/hubkit"
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency 'github_api', '~> 0.16.0'
|
29
|
+
spec.add_dependency 'activesupport', '>= 4', '< 6'
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
end
|
data/lib/hubkit.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/core_ext/module'
|
2
|
+
require 'active_support/core_ext/object'
|
3
|
+
require 'active_support/json'
|
4
|
+
require 'active_support/time'
|
5
|
+
|
6
|
+
require 'github_api'
|
7
|
+
require 'hubkit/configuration'
|
8
|
+
require 'hubkit/chainable_collection'
|
9
|
+
require 'hubkit/paginator'
|
10
|
+
require 'hubkit/cooldowner'
|
11
|
+
require 'hubkit/event_collection'
|
12
|
+
require 'hubkit/event_paginator'
|
13
|
+
require 'hubkit/issue_collection'
|
14
|
+
require 'hubkit/issue_paginator'
|
15
|
+
require 'hubkit/issue'
|
16
|
+
require 'hubkit/logger'
|
17
|
+
require 'hubkit/repo_collection'
|
18
|
+
require 'hubkit/repo_paginator'
|
19
|
+
require 'hubkit/repo'
|
20
|
+
require 'hubkit/version'
|
21
|
+
|
22
|
+
# Main module of the hubkit library. This is generally not used directly, but
|
23
|
+
# through a subclass such as Hubkit::IssueCollection, Hubkit::EventCollection,
|
24
|
+
# Hubkit::Repo, etc.
|
25
|
+
module Hubkit
|
26
|
+
class << self
|
27
|
+
# Return the Github client used by the library
|
28
|
+
# @return [Github::Client] the github API client
|
29
|
+
def client
|
30
|
+
Configuration.client
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# @abstract A class which wraps an array (or Enumerable) and provides convenience
|
3
|
+
# methods for chainable filters, e.g.:
|
4
|
+
# @example
|
5
|
+
# repo.issues.unassigned.labeled('in progress')
|
6
|
+
class ChainableCollection
|
7
|
+
# Allows definition of new chainable filters within the class definition
|
8
|
+
# @example
|
9
|
+
# scope :unlabeled, -> { |collection| collection.reject(&:labeled?) }
|
10
|
+
# @param [String, Symbol] name the anem of the method
|
11
|
+
# @yieldparam ... any arguments needed by the block
|
12
|
+
def self.scope(name, &block)
|
13
|
+
define_method name do |*args|
|
14
|
+
wrap(
|
15
|
+
instance_exec(*args, &block),
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create a new ChainableCollection
|
21
|
+
# @param [Enumerable] inner the collection which will be wrapped in the
|
22
|
+
# Hubkit::ChainableCollection
|
23
|
+
def initialize(inner)
|
24
|
+
@inner = inner
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return a Hubkit::ChainableCollection containing all members for which
|
28
|
+
# the block is true. This new Hubkit::ChainableCollection will also be
|
29
|
+
# filterable in the same way.
|
30
|
+
# @yieldparam item the item to be evaluated by the block
|
31
|
+
def select(&block)
|
32
|
+
return wrap(@inner.select &block) if block_given?
|
33
|
+
wrap(@inner.select)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return a collection of the same type as `self`, containing `items`
|
37
|
+
# @param [Enumberable] items the items to be contained in the new
|
38
|
+
# collection
|
39
|
+
def wrap(items)
|
40
|
+
self.class.new(items)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check if a method is implemented by either this method or the wrapped
|
44
|
+
# collection
|
45
|
+
# @param [String, Symbol] name the name of the method to check
|
46
|
+
# @param [Boolean] include_all if true, will include private methods
|
47
|
+
# @return [Boolean] returns true if the collection or the wrapped
|
48
|
+
# collection implements the method
|
49
|
+
def respond_to?(name, include_all = false)
|
50
|
+
super || @inner.respond_to?(name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Call into the wrapped collection if a method has not been implemented
|
54
|
+
# on the ChainableCollection
|
55
|
+
# @param [String, Symbol] name the name of the method being called
|
56
|
+
# @param [Array] args the arguments to the method being called
|
57
|
+
# @yieldparam ... the parameters of the any block given to the method
|
58
|
+
# which is being called
|
59
|
+
# @return the value of the method on the inner collection as called
|
60
|
+
def method_missing(name, *args, &block)
|
61
|
+
return super unless @inner.respond_to?(name)
|
62
|
+
@inner.send(name, *args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a collection which will contain all elements which are contained
|
66
|
+
# in this ChainableCollection, but NOT matching any additional chained filters
|
67
|
+
# @example
|
68
|
+
# ChainableCollection.new([1, 2, 3, 4]).not.select(&:odd?) # even->[2, 4]
|
69
|
+
# @return [NotCollection] a collection which contains all elements of self
|
70
|
+
# which don't match additional filters
|
71
|
+
def not
|
72
|
+
NotCollection.new(self)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns true if the other collection contains the same elements
|
76
|
+
# @param [Enumerable] other collection to compare with
|
77
|
+
# @return [Boolean] true if this collection and the other contain the same
|
78
|
+
# elements
|
79
|
+
def ==(other)
|
80
|
+
other == self.to_a
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# A collection that lets you perform the inverse of a filter
|
85
|
+
# @example
|
86
|
+
# repo.issues.not.labeled('in progress')
|
87
|
+
class NotCollection
|
88
|
+
def initialize(base)
|
89
|
+
@base = base
|
90
|
+
end
|
91
|
+
|
92
|
+
# Any method of this class will be delegated down to the original
|
93
|
+
# ChainableCollection. The result of the method will be a
|
94
|
+
# ChainableCollection which contains all the elements not returned
|
95
|
+
# by the filter called.
|
96
|
+
# @param [String, Symbol] name the name of the method being called
|
97
|
+
# @param [Array] args the arguments to the method being called
|
98
|
+
# @yieldparam ... the parameters of the any block given to the method
|
99
|
+
# which is being called
|
100
|
+
# @return [ChainableCollection] all elements which do not match the chained
|
101
|
+
# filter in a new ChainableCollection
|
102
|
+
def method_missing(name, *args, &block)
|
103
|
+
@base.wrap(@base - @base.send(name, *args, &block))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Hold the configuration of the Hubkit library. Holds the client, default
|
3
|
+
# github organization, and the github library configuration options.
|
4
|
+
# @attr [Github::Client] client the underlying Github client used for library
|
5
|
+
# operations
|
6
|
+
# @attr [String] default_org the github organization that will be used if
|
7
|
+
# none is specified
|
8
|
+
# @attr [Hash] github_config the configuration to use for the github client
|
9
|
+
# as a Hash
|
10
|
+
module Configuration
|
11
|
+
class << self
|
12
|
+
attr_writer :client
|
13
|
+
attr_accessor :default_org
|
14
|
+
attr_accessor :github_config
|
15
|
+
|
16
|
+
# Set configuration for the library
|
17
|
+
# @param [Hash] opts the configuration options to set
|
18
|
+
# @yieldparam [Hubkit::Configuration] this configuration, which is passed
|
19
|
+
# to the block
|
20
|
+
# @return [Github::Client] the github client to be used by the library
|
21
|
+
def configure(opts = {}, &block)
|
22
|
+
hubkit_config = self
|
23
|
+
@client = Github.new do |github_config|
|
24
|
+
hubkit_config.github_config = github_config
|
25
|
+
yield hubkit_config
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# The underlying Github::Client that will be used for library operations
|
30
|
+
# @return [Github::Client] a Github::Client which will be used for
|
31
|
+
# library operations
|
32
|
+
def client
|
33
|
+
@client || Github.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Pass through all method calls which are not implemented directly on the
|
37
|
+
# Configuration to the client's configuration method
|
38
|
+
# @param [String, Symbol] name the name of the method which is being called
|
39
|
+
# @param [Array] args the array of arguments to the method
|
40
|
+
# @yieldparam ... any arguments to the block required by the underlying
|
41
|
+
# Github::Client method
|
42
|
+
# @return the same return value that the github configuration would return
|
43
|
+
def method_missing(name, *args, &block)
|
44
|
+
if github_config.present? && github_config.respond_to?(name, false)
|
45
|
+
return github_config.public_send(name, *args, &block)
|
46
|
+
end
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# An object that handles Github rate throttling by setting a delay and then
|
3
|
+
# retrying a block
|
4
|
+
class Cooldowner
|
5
|
+
# Perform an action, and if Github rejects it due to rate limit, sleep and
|
6
|
+
# try again later
|
7
|
+
# @yield
|
8
|
+
def self.with_cooldown
|
9
|
+
cooldown = 1
|
10
|
+
begin
|
11
|
+
yield
|
12
|
+
rescue Github::Error::Forbidden => e
|
13
|
+
Logger.warn "Sleeping for abuse (#{cooldown} seconds)"
|
14
|
+
sleep cooldown
|
15
|
+
cooldown = [2 * cooldown, 10].min
|
16
|
+
retry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# A collection of GitHub events with chainable filters
|
3
|
+
class EventCollection < ChainableCollection
|
4
|
+
# Put the list of events in reverse chronological order
|
5
|
+
# @return [EventCollection] the events in reverse chronological order
|
6
|
+
def reverse_chronological
|
7
|
+
wrap(
|
8
|
+
sort do |a, b|
|
9
|
+
Time.parse(b['created_at']) <=> Time.parse(a['created_at'])
|
10
|
+
end,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Put the list of events in chronological order
|
15
|
+
# @return [EventCollection] the events in chronological order
|
16
|
+
def chronological
|
17
|
+
wrap(
|
18
|
+
sort do |a, b|
|
19
|
+
Time.parse(a['created_at']) <=> Time.parse(b['created_at'])
|
20
|
+
end,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Filter to all the events which occur between start_dt and end_date,
|
25
|
+
# inclusive
|
26
|
+
# @param [Date, DateTime] start_dt the beginning of the included period (inclusive)
|
27
|
+
# @param [Date, DateTime] end_date the end of the included period (inclusive)
|
28
|
+
# @return [EventCollection] the events falling in the window
|
29
|
+
def between(start_dt, end_date)
|
30
|
+
wrap(
|
31
|
+
@inner.select do |event|
|
32
|
+
stamp = Time.parse(event['created_at'])
|
33
|
+
stamp >= start_dt && stamp <= end_date
|
34
|
+
end,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Filter to all events where an issue was labeled with a given label
|
39
|
+
# @param [String] label the label to search for
|
40
|
+
# @return [EventCollection] a collection containing all events where an
|
41
|
+
# issue was labeled with the given label
|
42
|
+
def labeled(label)
|
43
|
+
wrap(
|
44
|
+
@inner.select do |event|
|
45
|
+
event['event'] == 'labeled' &&
|
46
|
+
event['label'].name.downcase == label.downcase
|
47
|
+
end,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Filter to all events where an issue was closed
|
52
|
+
# @return [EventCollection] a collection containing all events where an
|
53
|
+
# issue was closed
|
54
|
+
def closed
|
55
|
+
wrap(@inner.select { |event| event.event == 'closed' })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Returns all events for a GitHub issues-- for example, labeling, unlabeling,
|
3
|
+
# closing, etc-- and handle pagination for you
|
4
|
+
class EventPaginator < Paginator
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# Initialize a new paginator for events from the API
|
8
|
+
# @param [String] org the github organization which contains the repo for
|
9
|
+
# which we'll gather events
|
10
|
+
# @param [String] repo the github repo name for which we'll gather events
|
11
|
+
# @param [optional Fixnum] issue_number if present, the number of the issue
|
12
|
+
# for which we'll sfind events
|
13
|
+
def initialize(org:, repo:, issue_number: nil)
|
14
|
+
@org = org
|
15
|
+
@repo = repo
|
16
|
+
@issue_number = issue_number
|
17
|
+
|
18
|
+
opts =
|
19
|
+
if issue_number.present?
|
20
|
+
{ issue_number: issue_number }
|
21
|
+
else
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
|
25
|
+
super() do |i|
|
26
|
+
Cooldowner.with_cooldown do
|
27
|
+
Hubkit.client.issues.events.list(
|
28
|
+
@org,
|
29
|
+
@repo,
|
30
|
+
opts.merge(page: i),
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/hubkit/issue.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Represents one issue in github
|
3
|
+
# @attr [String] org the github organization name of the issue
|
4
|
+
# @attr [String] repo the github repo name
|
5
|
+
# @attr [Fixnum] number the issue's number
|
6
|
+
class Issue
|
7
|
+
# A regex which matches a textual name of an issue i.e. dingus/123 or
|
8
|
+
# foobar/dingus/123
|
9
|
+
ISSUE_PATTERN = %r{
|
10
|
+
(
|
11
|
+
(?<org>[^/]+)/(?<repo>[^/]+)
|
12
|
+
|
|
13
|
+
(?<repo>[^/]+)
|
14
|
+
)
|
15
|
+
/
|
16
|
+
(?<number>[0-9]+)
|
17
|
+
}x
|
18
|
+
|
19
|
+
# Create an Issue model from the textual name of the issue
|
20
|
+
# @example
|
21
|
+
# Hubkit::Issue.from_name('foobar/dingus/123')
|
22
|
+
# @param [String] name a string name for an issue, like dingus/123 or
|
23
|
+
# foobar/dingus/123
|
24
|
+
# @return [Hubkit::Issue] the issue matching this org, repo, and number
|
25
|
+
def self.from_name(name)
|
26
|
+
new(**parameters_from_name(name))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parse an issue name into org, repo, and number
|
30
|
+
# @param [String] name a string like dingus/123 or foobar/dingus/123
|
31
|
+
# @return [Hash] a hash containing org:, repo:, and number: values
|
32
|
+
def self.parameters_from_name(name)
|
33
|
+
match_data = Issue::ISSUE_PATTERN.match(name)
|
34
|
+
{
|
35
|
+
org: match_data[:org] || Hubkit::Configuration.default_org,
|
36
|
+
repo: match_data[:repo],
|
37
|
+
number: match_data[:number],
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create an Issue model from a github api payload. Preseeds the github
|
42
|
+
# payload of the model to avoid refetching from the API later.
|
43
|
+
# @param [String] org the github organization of the issue
|
44
|
+
# @param [String] repo the repo name of the issue
|
45
|
+
# @param [Hash] gh the payload hash from the API
|
46
|
+
def self.from_gh(org:, repo:, gh:)
|
47
|
+
new(org: org, repo: repo, number: gh['number']).tap do |issue|
|
48
|
+
issue.instance_variable_set(:@_to_gh, gh)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :org, :repo, :number
|
53
|
+
|
54
|
+
# Create a new issue model
|
55
|
+
# @param [String] org the github organization name of the issue
|
56
|
+
# @param [String] repo the github repo name
|
57
|
+
# @param [Fixnum] number the issue's number
|
58
|
+
def initialize(org:, repo:, number:)
|
59
|
+
@org = org
|
60
|
+
@repo = repo
|
61
|
+
@number = number
|
62
|
+
end
|
63
|
+
|
64
|
+
# The issue payload from the github API
|
65
|
+
# @return [Github::Mash] a hash-like object of the github API response
|
66
|
+
def to_gh
|
67
|
+
@_to_gh ||= Github.issues.get(@org, @repo, @number).body
|
68
|
+
end
|
69
|
+
|
70
|
+
# A list of all of the current labels of the issue
|
71
|
+
# @return [Enumerable] the list of labels
|
72
|
+
def labels
|
73
|
+
@_labels ||= to_gh['labels'].map { |label| label.name.downcase }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add a label to an issue
|
77
|
+
# @param [String] label the label to add to the issue
|
78
|
+
# @return [Enumerable] the new list of labels including the addition
|
79
|
+
def label!(label)
|
80
|
+
Hubkit.client.issues.labels.add @org, @repo, @number, label
|
81
|
+
labels.append(label).uniq!
|
82
|
+
end
|
83
|
+
|
84
|
+
# Remove a label from an issue
|
85
|
+
# @yieldparam [String] label_name a current label. if the block returns
|
86
|
+
# true, this label will be removed from the list
|
87
|
+
# @return [Enumerable] the new list of labels after any removals
|
88
|
+
def unlabel!(&block)
|
89
|
+
fail('Block is required for unlabel') unless block_given?
|
90
|
+
to_gh.labels.map(&:name)
|
91
|
+
.select do |label_name|
|
92
|
+
yield label_name
|
93
|
+
end
|
94
|
+
.each do |label_name|
|
95
|
+
Hubkit.client.issues.labels.remove @org, @repo, @number, label_name: label_name
|
96
|
+
labels.delete(label_name)
|
97
|
+
end
|
98
|
+
labels
|
99
|
+
end
|
100
|
+
|
101
|
+
delegate :title, :number, :body, to: :to_gh
|
102
|
+
|
103
|
+
# Returns true if the issue is a pull request
|
104
|
+
# @return [Boolean] true if the issue is a pull request, otherwise false
|
105
|
+
def pull?
|
106
|
+
to_gh['pull_request'].present?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true if the issue is not a pull request
|
110
|
+
# @return [Boolean] false if the issue is a pull request, otherwise true
|
111
|
+
def true_issue?
|
112
|
+
!pull?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the time at which the issue was labeled `label`. It will return
|
116
|
+
# the time of the latest such event.
|
117
|
+
# @param [String] label the label to search for
|
118
|
+
# @return [Date] the date when that issue was labeled such.
|
119
|
+
def when_labeled(label)
|
120
|
+
return unless events.labeled(label).any?
|
121
|
+
Time.parse(events.labeled(label).first['created_at'])
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns when the issue was opened
|
125
|
+
# @return [DateTime] the DateTime when the issue was created
|
126
|
+
def when_opened
|
127
|
+
Time.parse(created_at)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns when the issue was closed
|
131
|
+
# @return [DateTime] the DateTime when the issue was closed
|
132
|
+
def when_closed
|
133
|
+
return unless events.closed.any?
|
134
|
+
Time.parse(events.closed.first['created_at'])
|
135
|
+
end
|
136
|
+
|
137
|
+
# The EventCollection of events related to this Issue
|
138
|
+
# @return [EventCollection] the events for this issue, in reverse chronological order
|
139
|
+
def events
|
140
|
+
@_events ||=
|
141
|
+
EventCollection.new(
|
142
|
+
event_paginator,
|
143
|
+
)
|
144
|
+
.reverse_chronological
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns a paginator for fetching all events for this issue
|
148
|
+
# @return [EventPaginator] the paginator for fetching all events for this issue
|
149
|
+
def event_paginator
|
150
|
+
EventPaginator.new(
|
151
|
+
org: @org,
|
152
|
+
repo: @repo,
|
153
|
+
issue_number: @number,
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return the list of usernames of every user ever assigned to an issue
|
158
|
+
# @return [Enumerable] the usernames of the users which have been assigned
|
159
|
+
# to the issue
|
160
|
+
def users_ever_assigned
|
161
|
+
events
|
162
|
+
.select { |event| event.event == 'assigned' }
|
163
|
+
.map { |event| event.assignee.login }
|
164
|
+
.uniq
|
165
|
+
end
|
166
|
+
|
167
|
+
# A plaintext version of the issue, with the name, title, and body
|
168
|
+
# @return [String] a string containing the name, title, and body, suitable
|
169
|
+
# for uses like export or chat integration
|
170
|
+
def to_text
|
171
|
+
"#{number}: #{title}\n---\n#{body}\n"
|
172
|
+
end
|
173
|
+
|
174
|
+
# Allow access to elements of the github payload by calling the names as
|
175
|
+
# methods
|
176
|
+
# @param [String, Symbol] name
|
177
|
+
# @param [Array] args should be empty, only needed to match the expected
|
178
|
+
# signature from Ruby
|
179
|
+
# @return the value of to_gh[name]
|
180
|
+
def method_missing(name, *args, &block)
|
181
|
+
return to_gh[name] if args.length == 0 && !block_given? && to_gh.key?(name)
|
182
|
+
super
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# A collection of GitHub issues with chainable filters
|
3
|
+
class IssueCollection < ChainableCollection
|
4
|
+
# Filters to issues which where at some point labeled with the given label
|
5
|
+
# regardless of whether it currently has that label or not
|
6
|
+
# @param [String] label the label in question
|
7
|
+
# @return [IssueCollection] a collection of issues which were ever labeled
|
8
|
+
# with that label
|
9
|
+
def ever_labeled(label)
|
10
|
+
wrap(
|
11
|
+
@inner.select do |issue|
|
12
|
+
issue.when_labeled(label).present?
|
13
|
+
end,
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return a collection of issues which are open
|
18
|
+
# @return [IssueCollection] the issues in the collection which are open
|
19
|
+
def open
|
20
|
+
wrap(@inner.select { |issue| issue.state == "open" })
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return a collection of issues which are closed
|
24
|
+
# @return [IssueCollection] the issues in the collection which are closed
|
25
|
+
def closed
|
26
|
+
wrap(@inner.select { |issue| issue.state == "closed" })
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return a collection of issues which are labeled with any of the given
|
30
|
+
# labels
|
31
|
+
# @param [Enumerable, String] labels if a string, then this is the single
|
32
|
+
# label used to filter issues. If it is an enumerable set of strings,
|
33
|
+
# any issues matching any of the labels will be returned.
|
34
|
+
# @return [IssueCollection] a collection of matching the label
|
35
|
+
def labeled(labels)
|
36
|
+
return wrap(labels.flat_map { |label| labeled(label) }) if labels.respond_to?(:map)
|
37
|
+
|
38
|
+
wrap(@inner.select do |issue|
|
39
|
+
issue.labels.include?(labels.downcase)
|
40
|
+
end)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return a collection of issues which are not labeled
|
44
|
+
# @return [IssueCollection] a collection of issues without labels
|
45
|
+
def unlabeled
|
46
|
+
wrap(@inner.select { |issue| issue.labels.empty? })
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return a collection of issues which have labels matching a pattern
|
50
|
+
# @param [Regex] label_pattern a pattern which issues must match to be
|
51
|
+
# included in the collection
|
52
|
+
# @return [IssueCollection] a collection of issues with labels matching
|
53
|
+
# the pattern
|
54
|
+
def labeled_like(label_pattern)
|
55
|
+
wrap(@inner.select do |issue|
|
56
|
+
issue.labels.any? { |label| label_pattern.match(label) }
|
57
|
+
end)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return issues which are not pull requests
|
61
|
+
# @return [IssueCollection] a collection of issues which are not pull
|
62
|
+
# requests
|
63
|
+
def true_issues
|
64
|
+
wrap(@inner.select(&:true_issue?))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return issues which are pull requests
|
68
|
+
# @return [IssueCollection] a collection of issues which are pull requests
|
69
|
+
def pulls
|
70
|
+
wrap(@inner.select(&:pull?))
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return issues which are unassigned
|
74
|
+
# @return [IssueCollection] a collection of issues which are unassigned
|
75
|
+
def unassigned
|
76
|
+
wrap(@inner.select { |issue| issue.assignee.nil? })
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns issues opened within a date window between start_dt and end_dt
|
80
|
+
# @param [Date] start_dt the beginning date of the filter window (inclusive)
|
81
|
+
# @param [Date] end_dt the end date of the filter window (exclusive)
|
82
|
+
# @return [IssueCollection] a collection of issues opened within the window
|
83
|
+
def opened_between(start_dt, end_dt)
|
84
|
+
wrap(
|
85
|
+
@inner.select do |issue|
|
86
|
+
start_dt <= issue.when_opened && issue.when_opened < end_dt
|
87
|
+
end,
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Group the issue collection by the current assignee
|
92
|
+
# @return [Hash] a hash, where the assignee's username is the key, and the
|
93
|
+
# values are IssueCollections of issues assigned to that user
|
94
|
+
def by_assignee
|
95
|
+
groups = group_by { |issue| issue.assignee.try(:login) }
|
96
|
+
groups.each_with_object(groups) do |(key, list), memo|
|
97
|
+
memo[key] = wrap(list)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Returns the list of issues for a repo, handling pagination for you
|
3
|
+
class IssuePaginator < Paginator
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Initialize a new paginator for issues from the API
|
7
|
+
# @param [String] org the github organization which contains the repo for
|
8
|
+
# which we'll gather issues
|
9
|
+
# @param [String] repo the github repo name for which we'll gather issues
|
10
|
+
# @param [optional String] state if missing or open, the paginator will
|
11
|
+
# only have open issues returned. If 'all', the paginator will give you
|
12
|
+
# open and closed issues.
|
13
|
+
def initialize(org:, repo:, state: 'open')
|
14
|
+
@org = org
|
15
|
+
@repo = repo
|
16
|
+
super() do |i|
|
17
|
+
Cooldowner.with_cooldown do
|
18
|
+
Github.issues.list(user: @org, repo: @repo, state: state, page: i)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Hubkit
|
4
|
+
# The logger Hubkit uses for diagnostic info. If used in a Rails app and
|
5
|
+
# that Rails app already has a logger, it will use that same logger.
|
6
|
+
# Prepends [HUBKIT] to all log statements for easy filtering.
|
7
|
+
class Logger
|
8
|
+
# Write a debug level message to the log
|
9
|
+
# @param [String] msg the message to log
|
10
|
+
# @return [Boolean] true
|
11
|
+
def self.debug(msg)
|
12
|
+
inner_logger.debug('[HUBKIT] ' + msg)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Write a info level message to the log
|
16
|
+
# @param [String] msg the message to log
|
17
|
+
# @return [Boolean] true
|
18
|
+
def self.info(msg)
|
19
|
+
inner_logger.info('[HUBKIT] ' + msg)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Write a warning level message to the log
|
23
|
+
# @param [String] msg the message to log
|
24
|
+
# @return [Boolean] true
|
25
|
+
def self.warn(msg)
|
26
|
+
inner_logger.warn('[HUBKIT] ' + msg)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Write a error level message to the log
|
30
|
+
# @param [String] msg the message to log
|
31
|
+
# @return [Boolean] true
|
32
|
+
def self.error(msg)
|
33
|
+
inner_logger.error('[HUBKIT] ' + msg)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Write a fatal level message to the log
|
37
|
+
# @param [String] msg the message to log
|
38
|
+
# @return [Boolean] true
|
39
|
+
def self.fatal(msg)
|
40
|
+
inner_logger.fatal('[HUBKIT] ' + msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Write a unknown level message to the log
|
44
|
+
# @param [String] msg the message to log
|
45
|
+
# @return [Boolean] true
|
46
|
+
def self.unknown(msg)
|
47
|
+
inner_logger.unknown('[HUBKIT] ' + msg)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the ruby logger used by Hubkit
|
51
|
+
# @return [Logger] the logger. Rails is in use, will reuse the Rails
|
52
|
+
# logger.
|
53
|
+
def self.inner_logger
|
54
|
+
@_inner_logger ||=
|
55
|
+
if Kernel.const_defined? 'Rails'
|
56
|
+
Rails.logger
|
57
|
+
else
|
58
|
+
::Logger.new(STDERR)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# @abstract A class which implements an Enumerable which yields each resource returned
|
3
|
+
# by GitHub in turn, and handles the pagination for you (acts like a flat array,
|
4
|
+
# not an array of pages)
|
5
|
+
class Paginator
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Construct a new paginator
|
9
|
+
# @yieldparam [Object] result each page of results
|
10
|
+
def initialize(&block)
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
# Iterate through each page of the results and perform a block
|
15
|
+
# @yieldparam [Object] result each page of the
|
16
|
+
def each
|
17
|
+
i = 1
|
18
|
+
loop do
|
19
|
+
results = @block.call i
|
20
|
+
results.each do |result|
|
21
|
+
yield result
|
22
|
+
end
|
23
|
+
i += 1
|
24
|
+
break if results.length == 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/hubkit/repo.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Represents one GitHub repo
|
3
|
+
# @attr [String] org the github organization containing the repo
|
4
|
+
# @attr [String] repo the github repo name
|
5
|
+
class Repo
|
6
|
+
attr_accessor :org, :repo
|
7
|
+
|
8
|
+
# List available repos
|
9
|
+
# @param [optional String] visibility if missing or 'all', retrieves all
|
10
|
+
# repos. if 'public', only retrieves public repos
|
11
|
+
# @return [Enumerable] a list of Repo models accessible with the current
|
12
|
+
# credentials
|
13
|
+
def self.list(visibility='all')
|
14
|
+
RepoCollection.new(
|
15
|
+
RepoPaginator
|
16
|
+
.new(visibility)
|
17
|
+
.map { |repo| { org: repo.owner.login, repo: repo.name } }
|
18
|
+
.map { |params| new(**params) }
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Initialize a Repo model from a github API payload
|
23
|
+
# @param [Github::Mash] gh the github API response
|
24
|
+
# @return [Repo] the matching Repo model
|
25
|
+
def self.from_gh(gh)
|
26
|
+
new(org: gh['owner']['login'], repo: gh.name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initialize a Repo model from the name of the repo within the default
|
30
|
+
# organization
|
31
|
+
# @param [String] name the name of the repo
|
32
|
+
# @return [Repo] the matching Repo model
|
33
|
+
def self.from_name_with_default_org(name)
|
34
|
+
fail('Hubkit::Configuration.default_org is not set') if Hubkit::Configuration.default_org.nil?
|
35
|
+
return from_name(name) if name.include?('/')
|
36
|
+
from_name "#{Hubkit::Configuration.default_org}/#{name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize a Repo model from the organization and repo name
|
40
|
+
# @param [String] name the name of the organization/repo separated by a slash (/)
|
41
|
+
# @return [Repo] the matching Repo model
|
42
|
+
def self.from_name(name)
|
43
|
+
org, repo = name.split('/')
|
44
|
+
self.new(org: org, repo: repo)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Construct a Repo model from the organization and repo name
|
48
|
+
# @param [String] org the name of the github organization
|
49
|
+
# @param [String] repo the name of the github repo
|
50
|
+
def initialize(org:, repo:)
|
51
|
+
@org = org
|
52
|
+
@repo = repo
|
53
|
+
end
|
54
|
+
|
55
|
+
# A list of issues for this Repo
|
56
|
+
# @param [Boolean] include_closed if true, will include closed issue. if
|
57
|
+
# false or missing, the result will only include open issues
|
58
|
+
# @return [IssueCollection] the list of issues for the repo
|
59
|
+
def issues(include_closed = false)
|
60
|
+
issues_and_pulls(include_closed).true_issues
|
61
|
+
end
|
62
|
+
|
63
|
+
delegate :pulls, to: :issues_and_pulls
|
64
|
+
|
65
|
+
# A list of issues and pull requests for this Repo
|
66
|
+
# @param [Boolean] include_closed if true, will include closed issue. if
|
67
|
+
# false or missing, the result will only include open issues
|
68
|
+
# @return [IssueCollection] the list of issues and pulls for the repo
|
69
|
+
def issues_and_pulls(include_closed = false)
|
70
|
+
@_issues_and_pulls ||= {}
|
71
|
+
|
72
|
+
@_issues_and_pulls[include_closed] ||=
|
73
|
+
IssueCollection.new(
|
74
|
+
paginator_for_status(include_closed).to_a.flat_map do |gh|
|
75
|
+
Issue.from_gh(org: @org, repo: @repo, gh: gh)
|
76
|
+
end,
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get an IssuePaginator for a given open/closed issue status
|
81
|
+
# @param [Boolean] include_closed if true, paginator will return results
|
82
|
+
# with closed issues. if false, closed issues will be excluded
|
83
|
+
# @return [IssuePaginator] the issue paginator for this status and repo
|
84
|
+
def paginator_for_status(include_closed)
|
85
|
+
state_flag = include_closed ? 'all' : 'open'
|
86
|
+
|
87
|
+
IssuePaginator.new(
|
88
|
+
org: @org,
|
89
|
+
repo: @repo,
|
90
|
+
state: state_flag,
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get an array of label strings which are available on this repo
|
95
|
+
# @return [Enumerable] a list of strings which are the names of labels
|
96
|
+
# available on this repo
|
97
|
+
def labels
|
98
|
+
@_labels ||=
|
99
|
+
Github
|
100
|
+
.issues.labels.list(@org, @repo, per_page: 100)
|
101
|
+
.map(&:name)
|
102
|
+
.map(&:downcase)
|
103
|
+
.uniq
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return a human readable description of the Repo model
|
107
|
+
# @return [String] the human readable representation of the Repo model
|
108
|
+
# for the console
|
109
|
+
def inspect
|
110
|
+
"#<Hubkit::Repo:0x#{(object_id << 1).to_s(16)} #{@org}/#{@repo}>"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# A collection of Repos with chainable filters
|
3
|
+
class RepoCollection < ChainableCollection
|
4
|
+
scope :organization do |x|
|
5
|
+
@inner.select do |repo|
|
6
|
+
repo['owner']['login'] == x
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
scope :fork do
|
11
|
+
wrap(@inner.select { |repo| repo['fork'] })
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hubkit
|
2
|
+
# Retrieves all visible repos in one flat array, handling GitHub pagination
|
3
|
+
class RepoPaginator < Paginator
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Construct a new repo paginator
|
7
|
+
# @param [optional String] visibility if missing or 'all', retrieves all
|
8
|
+
# repos. if 'public', only retrieves public repos
|
9
|
+
def initialize(visibility='all')
|
10
|
+
super() do |i|
|
11
|
+
Cooldowner.with_cooldown do
|
12
|
+
Hubkit.client.repos.list(visibility: visibility, page: i)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hubkit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Prehn
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-15 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.16.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.16.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '6'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '4'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '6'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.14'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.14'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '10.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '10.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
description: |2
|
90
|
+
Hubkit provides methods for querying the github API at a higher level than
|
91
|
+
making individual API calls. Think of it like an ORM for the github API.
|
92
|
+
email:
|
93
|
+
- robert@revelry.co
|
94
|
+
executables: []
|
95
|
+
extensions: []
|
96
|
+
extra_rdoc_files: []
|
97
|
+
files:
|
98
|
+
- ".gitignore"
|
99
|
+
- ".rspec"
|
100
|
+
- ".travis.yml"
|
101
|
+
- CODE_OF_CONDUCT.md
|
102
|
+
- CONTRIBUTING.md
|
103
|
+
- Gemfile
|
104
|
+
- LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- bin/console
|
108
|
+
- bin/setup
|
109
|
+
- hubkit.gemspec
|
110
|
+
- lib/hubkit.rb
|
111
|
+
- lib/hubkit/chainable_collection.rb
|
112
|
+
- lib/hubkit/configuration.rb
|
113
|
+
- lib/hubkit/cooldowner.rb
|
114
|
+
- lib/hubkit/event_collection.rb
|
115
|
+
- lib/hubkit/event_paginator.rb
|
116
|
+
- lib/hubkit/issue.rb
|
117
|
+
- lib/hubkit/issue_collection.rb
|
118
|
+
- lib/hubkit/issue_paginator.rb
|
119
|
+
- lib/hubkit/logger.rb
|
120
|
+
- lib/hubkit/paginator.rb
|
121
|
+
- lib/hubkit/repo.rb
|
122
|
+
- lib/hubkit/repo_collection.rb
|
123
|
+
- lib/hubkit/repo_paginator.rb
|
124
|
+
- lib/hubkit/version.rb
|
125
|
+
homepage: https://github.com/prehnRA/hubkit
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata: {}
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.5.1
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Higher level abstractions for querying the github API
|
149
|
+
test_files: []
|
150
|
+
has_rdoc:
|