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