octopi 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +166 -0
- data/VERSION.yml +4 -0
- data/lib/octopi.rb +210 -0
- data/lib/octopi/base.rb +109 -0
- data/lib/octopi/blob.rb +21 -0
- data/lib/octopi/branch.rb +17 -0
- data/lib/octopi/commit.rb +64 -0
- data/lib/octopi/error.rb +23 -0
- data/lib/octopi/file_object.rb +15 -0
- data/lib/octopi/issue.rb +98 -0
- data/lib/octopi/key.rb +18 -0
- data/lib/octopi/repository.rb +79 -0
- data/lib/octopi/resource.rb +66 -0
- data/lib/octopi/tag.rb +17 -0
- data/lib/octopi/user.rb +69 -0
- data/test/octopi_test.rb +46 -0
- data/test/test_helper.rb +10 -0
- metadata +74 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Felipe Coury
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
= octopi
|
2
|
+
|
3
|
+
Octopi is a Ruby interface to GitHub API v2 (http://develop.github.com).
|
4
|
+
|
5
|
+
To install it as a Gem, just run:
|
6
|
+
|
7
|
+
$ sudo gem install fcoury-octopi --source http://gems.github.com
|
8
|
+
|
9
|
+
== Authenticated Usage
|
10
|
+
|
11
|
+
=== Seamless authentication using .gitconfig defaults
|
12
|
+
|
13
|
+
If you have your <tt>~/.gitconfig</tt> file in place, and you have a [github] section (if you don't, take a look at this GitHub Guides entry: http://github.com/guides/tell-git-your-user-name-and-email-address), you can use seamless authentication using this method:
|
14
|
+
|
15
|
+
authenticated do |g|
|
16
|
+
repo = g.repository("api-labrat")
|
17
|
+
(...)
|
18
|
+
end
|
19
|
+
|
20
|
+
=== Explicit authentication
|
21
|
+
|
22
|
+
Sometimes, you may not want to get authentication data from <tt>~/.gitconfig</tt>. You want to use GitHub API authenticated as a third party. For this use case, you have a couple of options too.
|
23
|
+
|
24
|
+
<b>1. Providing login and token inline:</b>
|
25
|
+
|
26
|
+
authenticated_with "mylogin", "mytoken" do |g|
|
27
|
+
repo = g.repository("api-labrat")
|
28
|
+
issue = repo.open_issue :title => "Sample issue",
|
29
|
+
:body => "This issue was opened using GitHub API and Octopi"
|
30
|
+
puts issue.number
|
31
|
+
end
|
32
|
+
|
33
|
+
<b>2. Providing a YAML file with authentication information:</b>
|
34
|
+
|
35
|
+
Use the following format:
|
36
|
+
|
37
|
+
#
|
38
|
+
# Octopi GitHub API configuration file
|
39
|
+
#
|
40
|
+
|
41
|
+
# GitHub user login and token
|
42
|
+
login: github-username
|
43
|
+
token: github-token
|
44
|
+
|
45
|
+
# Trace level
|
46
|
+
# Possible values:
|
47
|
+
# false - no tracing, same as if the param is ommited
|
48
|
+
# true - will output each POST or GET operation to the stdout
|
49
|
+
# curl - same as true, but in addition will output the curl equivalent of each command (for debugging)
|
50
|
+
trace: curl
|
51
|
+
|
52
|
+
And change the way you connect to:
|
53
|
+
|
54
|
+
authenticated_with :config => "github.yml" do |g|
|
55
|
+
(...)
|
56
|
+
end
|
57
|
+
|
58
|
+
== Anonymous Usage
|
59
|
+
|
60
|
+
This reflects the usage of the API to retrieve information on a read-only fashion, where the user doesn't have to be authenticated.
|
61
|
+
|
62
|
+
=== Users API
|
63
|
+
|
64
|
+
Getting user information
|
65
|
+
|
66
|
+
user = User.find("fcoury")
|
67
|
+
puts "#{user.name} is being followed by #{user.followers.join(", ")} and following #{user.following.join(", ")}"
|
68
|
+
|
69
|
+
The bang methods `followers!` and `following!` retrieves a full User object for each user login returned, so it has to be used carefully.
|
70
|
+
|
71
|
+
user.followers!.each do |u|
|
72
|
+
puts " - #{u.name} (#{u.login}) has #{u.public_repo_count} repo(s)"
|
73
|
+
end
|
74
|
+
|
75
|
+
Searching for user
|
76
|
+
|
77
|
+
users = User.find_all("silva")
|
78
|
+
puts "#{users.size} users found for 'silva':"
|
79
|
+
users.each do |u|
|
80
|
+
puts " - #{u.name}"
|
81
|
+
end
|
82
|
+
|
83
|
+
=== Repositories API
|
84
|
+
|
85
|
+
repo = user.repository("octopi") # same as: Repository.find("fcoury", "octopi")
|
86
|
+
puts "Repository: #{repo.name} - #{repo.description} (by #{repo.owner}) - #{repo.url}"
|
87
|
+
puts " Tags: #{repo.tags and repo.tags.map {|t| t.name}.join(", ")}"
|
88
|
+
|
89
|
+
Search:
|
90
|
+
|
91
|
+
repos = Repository.find_all("ruby", "git")
|
92
|
+
puts "#{repos.size} repository(ies) with 'ruby' and 'git':"
|
93
|
+
repos.each do |r|
|
94
|
+
puts " - #{r.name}"
|
95
|
+
end
|
96
|
+
|
97
|
+
Issues API integrated into the Repository object:
|
98
|
+
|
99
|
+
issue = repo.issues.first
|
100
|
+
puts "First open issue: #{issue.number} - #{issue.title} - Created at: #{issue.created_at}"
|
101
|
+
|
102
|
+
Single issue information:
|
103
|
+
|
104
|
+
issue = repo.issue(11)
|
105
|
+
|
106
|
+
Commits API information from a Repository object:
|
107
|
+
|
108
|
+
first_commit = repo.commits.first
|
109
|
+
puts "First commit: #{first_commit.id} - #{first_commit.message} - by #{first_commit.author['name']}"
|
110
|
+
|
111
|
+
Single commit information:
|
112
|
+
|
113
|
+
puts "Diff:"
|
114
|
+
first_commit.details.modified.each {|m| puts "#{m['filename']} DIFF: #{m['diff']}" }
|
115
|
+
|
116
|
+
== Tracing
|
117
|
+
|
118
|
+
=== Levels
|
119
|
+
|
120
|
+
You can can use tracing to enable better debugging output when something goes wrong. There are 3 tracing levels:
|
121
|
+
|
122
|
+
* false (default) - no tracing
|
123
|
+
* true - will output each GET and POST calls, along with URL and params
|
124
|
+
* curl - same as true, but additionally outputs the curl command to replicate the issue
|
125
|
+
|
126
|
+
If you choose curl tracing, the curl command equivalent to each command sent to GitHub will be output to the stdout, like this example:
|
127
|
+
|
128
|
+
=> Trace on: curl
|
129
|
+
POST: /issues/open/webbynode/api-labrat params: body=This issue was opened using GitHub API and Octopi, title=Sample issue
|
130
|
+
===== curl version
|
131
|
+
curl -F 'body=This issue was opened using GitHub API and Octopi' -F 'login=mylogin' -F 'token=mytoken' -F 'title=Sample issue' http://github.com/api/v2/issues/open/webbynode/api-labrat
|
132
|
+
==================
|
133
|
+
|
134
|
+
=== Enabling
|
135
|
+
|
136
|
+
Tracing can be enabled in different ways, depending on the API feature you're using:
|
137
|
+
|
138
|
+
<b>Anonymous (this will be improved later):</b>
|
139
|
+
|
140
|
+
ANONYMOUS_API.trace_level = "trace-level"
|
141
|
+
|
142
|
+
<b>Seamless authenticated</b>
|
143
|
+
|
144
|
+
authenticated :trace => "trace-level" do |g|; ...; end
|
145
|
+
|
146
|
+
<b>Explicitly authenticated</b>
|
147
|
+
|
148
|
+
Current version of explicit authentication requires a :config param to a YAML file to allow tracing. For enabling tracing on a YAML file refer to the config.yml example presented on the Explicit authentication section.
|
149
|
+
|
150
|
+
== Author
|
151
|
+
|
152
|
+
* Felipe Coury - http://felipecoury.com
|
153
|
+
* HasMany.info blog - http://hasmany.info
|
154
|
+
|
155
|
+
== Contributors
|
156
|
+
|
157
|
+
In alphabetical order:
|
158
|
+
|
159
|
+
* Brandon Calloway - http://github.com/bcalloway
|
160
|
+
* runpaint - http://github.com/runpaint
|
161
|
+
|
162
|
+
Thanks guys!
|
163
|
+
|
164
|
+
== Copyright
|
165
|
+
|
166
|
+
Copyright (c) 2009 Felipe Coury. See LICENSE for details.
|
data/VERSION.yml
ADDED
data/lib/octopi.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'yaml'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module Octopi
|
7
|
+
class Api; end
|
8
|
+
ANONYMOUS_API = Api.new
|
9
|
+
|
10
|
+
def authenticated(*args, &block)
|
11
|
+
opts = args.last.is_a?(Hash) ? args.last : {}
|
12
|
+
config = read_gitconfig
|
13
|
+
login = config["github"]["user"]
|
14
|
+
token = config["github"]["token"]
|
15
|
+
|
16
|
+
api = Api.new(login, token)
|
17
|
+
api.trace_level = opts[:trace]
|
18
|
+
|
19
|
+
puts "=> Trace on: #{api.trace_level}" if api.trace_level
|
20
|
+
|
21
|
+
yield api
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticated_with(*args, &block)
|
25
|
+
opts = args.last.is_a?(Hash) ? args.last : {}
|
26
|
+
if opts[:config]
|
27
|
+
config = File.open(opts[:config]) { |yf| YAML::load(yf) }
|
28
|
+
raise "Missing config #{opts[:config]}" unless config
|
29
|
+
|
30
|
+
login = config["login"]
|
31
|
+
token = config["token"]
|
32
|
+
trace = config["trace"]
|
33
|
+
else
|
34
|
+
login, token = *args
|
35
|
+
end
|
36
|
+
|
37
|
+
puts "=> Trace on: #{trace}" if trace
|
38
|
+
|
39
|
+
api = Api.new(login, token)
|
40
|
+
api.trace_level = trace if trace
|
41
|
+
yield api
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_gitconfig
|
45
|
+
config = {}
|
46
|
+
group = nil
|
47
|
+
File.foreach("#{ENV['HOME']}/.gitconfig") do |line|
|
48
|
+
line.strip!
|
49
|
+
if line[0] != ?# and line =~ /\S/
|
50
|
+
if line =~ /^\[(.*)\]$/
|
51
|
+
group = $1
|
52
|
+
else
|
53
|
+
key, value = line.split("=")
|
54
|
+
(config[group]||={})[key.strip] = value.strip
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
config
|
59
|
+
end
|
60
|
+
|
61
|
+
class Api
|
62
|
+
include HTTParty
|
63
|
+
CONTENT_TYPE = {
|
64
|
+
'yaml' => 'application/x-yaml',
|
65
|
+
'json' => 'application/json',
|
66
|
+
'xml' => 'application/sml'
|
67
|
+
}
|
68
|
+
RETRYABLE_STATUS = [403]
|
69
|
+
MAX_RETRIES = 10
|
70
|
+
|
71
|
+
base_uri "http://github.com/api/v2"
|
72
|
+
|
73
|
+
attr_accessor :format, :login, :token, :trace_level, :read_only
|
74
|
+
|
75
|
+
def initialize(login = nil, token = nil, format = "yaml")
|
76
|
+
@format = format
|
77
|
+
@read_only = true
|
78
|
+
|
79
|
+
if login
|
80
|
+
@login = login
|
81
|
+
@token = token
|
82
|
+
@read_only = false
|
83
|
+
self.class.default_params :login => login, :token => token
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def read_only?
|
88
|
+
read_only
|
89
|
+
end
|
90
|
+
|
91
|
+
{:keys => 'public_keys', :emails => 'emails'}.each_pair do |action, key|
|
92
|
+
define_method("#{action}") do
|
93
|
+
get("/user/#{action}")[key]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def user
|
98
|
+
user_data = get("/user/show/#{login}")
|
99
|
+
raise "Unexpected response for user command" unless user_data and user_data['user']
|
100
|
+
User.new(self, user_data['user'])
|
101
|
+
end
|
102
|
+
|
103
|
+
def open_issue(user, repo, params)
|
104
|
+
Issue.open(user, repo, params, self)
|
105
|
+
end
|
106
|
+
|
107
|
+
def repository(name)
|
108
|
+
repo = Repository.find(user, name, self)
|
109
|
+
repo.api = self
|
110
|
+
repo
|
111
|
+
end
|
112
|
+
alias_method :repo, :repository
|
113
|
+
|
114
|
+
def save(resource_path, data)
|
115
|
+
traslate resource_path, data
|
116
|
+
#still can't figure out on what format values are expected
|
117
|
+
post("#{resource_path}", { :query => data })
|
118
|
+
end
|
119
|
+
|
120
|
+
def find(path, result_key, resource_id)
|
121
|
+
get(path, { :id => resource_id })
|
122
|
+
end
|
123
|
+
|
124
|
+
def find_all(path, result_key, query)
|
125
|
+
get(path, { :query => query, :id => query })[result_key]
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_raw(path, params)
|
129
|
+
get(path, params, 'plain')
|
130
|
+
end
|
131
|
+
|
132
|
+
def get(path, params = {}, format = "yaml")
|
133
|
+
@@retries = 0
|
134
|
+
begin
|
135
|
+
trace "GET [#{format}]", "/#{format}#{path}", params
|
136
|
+
submit(path, params, format) do |path, params, format|
|
137
|
+
self.class.get "/#{format}#{path}"
|
138
|
+
end
|
139
|
+
rescue RetryableAPIError => e
|
140
|
+
if @@retries < MAX_RETRIES
|
141
|
+
$stderr.puts e.message
|
142
|
+
@@retries += 1
|
143
|
+
retry
|
144
|
+
else
|
145
|
+
raise APIError, "GitHub returned status #{e.code}, despite" +
|
146
|
+
" repeating the request #{MAX_RETRIES} times. Giving up."
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def post(path, params = {}, format = "yaml")
|
152
|
+
trace "POST", "/#{format}#{path}", params
|
153
|
+
submit(path, params, format) do |path, params, format|
|
154
|
+
resp = self.class.post "/#{format}#{path}", :query => params
|
155
|
+
resp
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
def submit(path, params = {}, format = "yaml", &block)
|
161
|
+
params.each_pair do |k,v|
|
162
|
+
if path =~ /:#{k.to_s}/
|
163
|
+
params.delete(k)
|
164
|
+
path = path.gsub(":#{k.to_s}", v)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
query = login ? { :login => login, :token => token } : {}
|
168
|
+
query.merge!(params)
|
169
|
+
|
170
|
+
begin
|
171
|
+
resp = yield(path, query.merge(params), format)
|
172
|
+
rescue Net::HTTPBadResponse
|
173
|
+
raise RetryableAPIError
|
174
|
+
end
|
175
|
+
|
176
|
+
if @trace_level
|
177
|
+
case @trace_level
|
178
|
+
when "curl"
|
179
|
+
query_trace = []
|
180
|
+
query.each_pair { |k,v| query_trace << "-F '#{k}=#{v}'" }
|
181
|
+
puts "===== [curl version]"
|
182
|
+
puts "curl #{query_trace.join(" ")} #{self.class.base_uri}/#{format}#{path}"
|
183
|
+
puts "===================="
|
184
|
+
end
|
185
|
+
end
|
186
|
+
raise RetryableAPIError, resp.code.to_i if RETRYABLE_STATUS.include? resp.code.to_i
|
187
|
+
raise APIError,
|
188
|
+
"GitHub returned status #{resp.code}" unless resp.code.to_i == 200
|
189
|
+
# FIXME: This fails for showing raw Git data because that call returns
|
190
|
+
# text/html as the content type. This issue has been reported.
|
191
|
+
ctype = resp.headers['content-type'].first
|
192
|
+
raise FormatError, [ctype, format] unless
|
193
|
+
ctype.match(/^#{CONTENT_TYPE[format]};/)
|
194
|
+
if format == 'yaml' && resp['error']
|
195
|
+
raise APIError, resp['error'].first['error']
|
196
|
+
end
|
197
|
+
resp
|
198
|
+
end
|
199
|
+
|
200
|
+
def trace(oper, url, params)
|
201
|
+
return unless trace_level
|
202
|
+
par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params and !params.empty?
|
203
|
+
puts "#{oper}: #{url}#{par_str}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
%w{error base resource user tag repository issue file_object blob key commit branch}.
|
208
|
+
each{|f| require "#{File.dirname(__FILE__)}/octopi/#{f}"}
|
209
|
+
|
210
|
+
end
|
data/lib/octopi/base.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
class String
|
2
|
+
def camel_case
|
3
|
+
self.gsub(/(^|_)(.)/) { $2.upcase }
|
4
|
+
end
|
5
|
+
end
|
6
|
+
module Octopi
|
7
|
+
class Base
|
8
|
+
VALID = {
|
9
|
+
:repo => {
|
10
|
+
# FIXME: API currently chokes on repository names containing periods,
|
11
|
+
# but presumably this will be fixed.
|
12
|
+
:pat => /^[a-z0-9_\.-]+$/,
|
13
|
+
:msg => "%s is an invalid repository name"},
|
14
|
+
:user => {
|
15
|
+
:pat => /^[A-Za-z0-9_\.-]+$/,
|
16
|
+
:msg => "%s is an invalid username"},
|
17
|
+
:file => {
|
18
|
+
:pat => /^[^ \/]+$/,
|
19
|
+
:msg => "%s is an invalid filename"},
|
20
|
+
:sha => {
|
21
|
+
:pat => /^[a-f0-9]{40}$/,
|
22
|
+
:msg => "%s is an invalid SHA hash"},
|
23
|
+
:state => {
|
24
|
+
# FIXME: Any way to access Issue::STATES from here?
|
25
|
+
:pat => /^(open|closed)$/,
|
26
|
+
:msg => "%s is an invalid state; should be 'open' or 'closed'."
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
attr_accessor :api
|
31
|
+
|
32
|
+
def initialize(api, hash)
|
33
|
+
@api = api
|
34
|
+
@keys = []
|
35
|
+
|
36
|
+
raise "Missing data for #{@resource}" unless hash
|
37
|
+
|
38
|
+
hash.each_pair do |k,v|
|
39
|
+
@keys << k
|
40
|
+
next if k =~ /\./
|
41
|
+
instance_variable_set("@#{k}", v)
|
42
|
+
|
43
|
+
method = (TrueClass === v || FalseClass === v) ? "#{k}?" : k
|
44
|
+
|
45
|
+
self.class.send :define_method, "#{method}=" do |v|
|
46
|
+
instance_variable_set("@#{k}", v)
|
47
|
+
end
|
48
|
+
|
49
|
+
self.class.send :define_method, method do
|
50
|
+
instance_variable_get("@#{k}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def property(p, v)
|
56
|
+
path = "#{self.class.path_for(:resource)}/#{p}"
|
57
|
+
@api.find(path, self.class.resource_name(:singular), v)
|
58
|
+
end
|
59
|
+
|
60
|
+
def save
|
61
|
+
hash = {}
|
62
|
+
@keys.each { |k| hash[k] = send(k) }
|
63
|
+
@api.save(self.path_for(:resource), hash)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def self.extract_user_repository(*args)
|
68
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
69
|
+
if opts.empty?
|
70
|
+
user, repo = *args if args.length > 1
|
71
|
+
repo ||= args.first
|
72
|
+
else
|
73
|
+
opts[:repo] = opts[:repository] if opts[:repository]
|
74
|
+
repo = args.pop || opts[:repo]
|
75
|
+
user = opts[:user]
|
76
|
+
end
|
77
|
+
|
78
|
+
user ||= repo.owner if repo.is_a? Repository
|
79
|
+
|
80
|
+
if repo.is_a?(String) and !user
|
81
|
+
raise "Need user argument when repository is identified by name"
|
82
|
+
end
|
83
|
+
|
84
|
+
ret = extract_names(user, repo)
|
85
|
+
ret << opts
|
86
|
+
ret
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.extract_names(*args)
|
90
|
+
args.map do |v|
|
91
|
+
v = v.name if v.is_a? Repository
|
92
|
+
v = v.login if v.is_a? User
|
93
|
+
v
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.validate_args(spec)
|
98
|
+
m = caller[0].match(/\/([a-z0-9_]+)\.rb:\d+:in `([a-z_0-9]+)'/)
|
99
|
+
meth = m ? "#{m[1].camel_case}.#{m[2]}" : 'method'
|
100
|
+
raise ArgumentError, "Invalid spec" unless
|
101
|
+
spec.values.all? { |s| VALID.key? s }
|
102
|
+
errors = spec.reject{|arg, spec| arg.nil?}.
|
103
|
+
reject{|arg, spec| arg.to_s.match(VALID[spec][:pat])}.
|
104
|
+
map {|arg, spec| "Invalid argument '%s' for %s (%s)" %
|
105
|
+
[arg, meth, VALID[spec][:msg] % arg]}
|
106
|
+
raise ArgumentError, "\n" + errors.join("\n") unless errors.empty?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/octopi/blob.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Blob < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "blob"
|
5
|
+
|
6
|
+
resource_path "/blob/show/:id"
|
7
|
+
|
8
|
+
def self.find(user, repo, sha, path=nil)
|
9
|
+
user = user.login if user.is_a? User
|
10
|
+
repo = repo.name if repo.is_a? Repository
|
11
|
+
self.class.validate_args(sha => :sha, user => :user, path => :file)
|
12
|
+
if path
|
13
|
+
super [user,repo,sha,path]
|
14
|
+
else
|
15
|
+
blob = ANONYMOUS_API.get_raw(path_for(:resource),
|
16
|
+
{:id => [user,repo,sha].join('/')})
|
17
|
+
new(ANONYMOUS_API, {:text => blob})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Branch < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "branch", "branches"
|
5
|
+
|
6
|
+
resource_path "/repos/show/:id"
|
7
|
+
|
8
|
+
def self.find(user, repo)
|
9
|
+
user = user.login if user.is_a? User
|
10
|
+
repo = repo.name if repo.is_a? Repository
|
11
|
+
self.validate_args(user => :user, repo => :repo)
|
12
|
+
find_plural([user,repo,'branches'], :resource){
|
13
|
+
|i| {:name => i.first, :hash => i.last }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Commit < Base
|
3
|
+
include Resource
|
4
|
+
find_path "/commits/list/:query"
|
5
|
+
resource_path "/commits/show/:id"
|
6
|
+
|
7
|
+
attr_accessor :repository
|
8
|
+
|
9
|
+
# Finds all commits for a given Repository's branch
|
10
|
+
#
|
11
|
+
# You can provide the user and repo parameters as
|
12
|
+
# String or as User and Repository objects. When repo
|
13
|
+
# is provided as a Repository object, user is superfluous.
|
14
|
+
#
|
15
|
+
# If no branch is given, "master" is assumed.
|
16
|
+
#
|
17
|
+
# Sample usage:
|
18
|
+
#
|
19
|
+
# find_all(repo, :branch => "develop") # repo must be an object
|
20
|
+
# find_all("octopi", :user => "fcoury") # user must be provided
|
21
|
+
# find_all(:user => "fcoury", :repo => "octopi") # branch defaults to master
|
22
|
+
#
|
23
|
+
def self.find_all(*args)
|
24
|
+
repo = args.first
|
25
|
+
user ||= repo.owner if repo.is_a? Repository
|
26
|
+
user, repo_name, opts = extract_user_repository(*args)
|
27
|
+
self.validate_args(user => :user, repo_name => :repo)
|
28
|
+
branch = opts[:branch] || "master"
|
29
|
+
|
30
|
+
commits = super user, repo_name, branch
|
31
|
+
commits.each { |c| c.repository = repo } if repo.is_a? Repository
|
32
|
+
commits
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: Make find use hashes like find_all
|
36
|
+
def self.find(*args)
|
37
|
+
if args.last.is_a?(Commit)
|
38
|
+
commit = args.pop
|
39
|
+
super "#{commit.repo_identifier}"
|
40
|
+
else
|
41
|
+
user, name, sha = *args
|
42
|
+
user = user.login if user.is_a? User
|
43
|
+
name = repo.name if name.is_a? Repository
|
44
|
+
self.validate_args(user => :user, name => :repo, sha => :sha)
|
45
|
+
super [user, name, sha]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def details
|
50
|
+
self.class.find(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def repo_identifier
|
54
|
+
url_parts = url.split('/')
|
55
|
+
if @repository
|
56
|
+
parts = [@repository.owner, @repository.name, url_parts[6]]
|
57
|
+
else
|
58
|
+
parts = [url_parts[3], url_parts[4], url_parts[6]]
|
59
|
+
end
|
60
|
+
|
61
|
+
parts.join('/')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/octopi/error.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Octopi
|
2
|
+
|
3
|
+
class FormatError < StandardError
|
4
|
+
def initialize(f)
|
5
|
+
$stderr.puts "Got unexpected format (got #{f.first} for #{f.last})"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class APIError < StandardError
|
10
|
+
def initialize(m)
|
11
|
+
$stderr.puts m
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RetryableAPIError < RuntimeError
|
16
|
+
attr_reader :code
|
17
|
+
def initialize(code=nil)
|
18
|
+
@code = code.nil? ? '???' : code
|
19
|
+
@message = "GitHub returned status #{@code}. Retrying request."
|
20
|
+
super @message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Octopi
|
2
|
+
class FileObject < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "tree"
|
5
|
+
|
6
|
+
resource_path "/tree/show/:id"
|
7
|
+
|
8
|
+
def self.find(user, repo, sha)
|
9
|
+
user = user.login if user.is_a? User
|
10
|
+
repo = repo.name if repo.is_a? Repository
|
11
|
+
self.validate_args(sha => :sha, user => :user, repo => :repo)
|
12
|
+
super [user,repo,sha]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/octopi/issue.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Issue < Base
|
3
|
+
include Resource
|
4
|
+
STATES = %w{open closed}
|
5
|
+
|
6
|
+
find_path "/issues/list/:query"
|
7
|
+
resource_path "/issues/show/:id"
|
8
|
+
|
9
|
+
attr_accessor :repository
|
10
|
+
|
11
|
+
# Finds all issues for a given Repository
|
12
|
+
#
|
13
|
+
# You can provide the user and repo parameters as
|
14
|
+
# String or as User and Repository objects. When repo
|
15
|
+
# is provided as a Repository object, user is superfluous.
|
16
|
+
#
|
17
|
+
# If no state is given, "open" is assumed.
|
18
|
+
#
|
19
|
+
# Sample usage:
|
20
|
+
#
|
21
|
+
# find_all(repo, :state => "closed") # repo must be an object
|
22
|
+
# find_all("octopi", :user => "fcoury") # user must be provided
|
23
|
+
# find_all(:user => "fcoury", :repo => "octopi") # state defaults to open
|
24
|
+
#
|
25
|
+
def self.find_all(*args)
|
26
|
+
repo = args.first
|
27
|
+
user, repo_name, opts = extract_user_repository(*args)
|
28
|
+
state = opts[:state] || "open"
|
29
|
+
state.downcase! if state
|
30
|
+
validate_args(user => :user, repo_name => :repo, state => :state)
|
31
|
+
|
32
|
+
issues = super user, repo_name, state
|
33
|
+
issues.each { |i| i.repository = repo } if repo.is_a? Repository
|
34
|
+
issues
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO: Make find use hashes like find_all
|
38
|
+
def self.find(*args)
|
39
|
+
if args.length < 2
|
40
|
+
raise "Issue.find needs user, repository and issue number"
|
41
|
+
end
|
42
|
+
|
43
|
+
number = args.pop.to_i if args.last.respond_to?(:to_i)
|
44
|
+
number = args.pop if args.last.is_a?(Integer)
|
45
|
+
|
46
|
+
raise "Issue.find needs issue number as the last argument" unless number
|
47
|
+
|
48
|
+
if args.length > 1
|
49
|
+
user, repo = *args
|
50
|
+
else
|
51
|
+
repo = args.pop
|
52
|
+
raise "Issue.find needs at least a Repository object and issue number" unless repo.is_a? Repository
|
53
|
+
user, repo = repo.owner, repo.name
|
54
|
+
end
|
55
|
+
|
56
|
+
user, repo = extract_names(user, repo)
|
57
|
+
validate_args(user => :user, repo => :repo)
|
58
|
+
super user, repo, number
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.open(user, repo, params, api = ANONYMOUS_API)
|
62
|
+
user, repo_name = extract_names(user, repo)
|
63
|
+
data = api.post("/issues/open/#{user}/#{repo_name}", params)
|
64
|
+
issue = new(api, data['issue'])
|
65
|
+
issue.repository = repo if repo.is_a? Repository
|
66
|
+
issue
|
67
|
+
end
|
68
|
+
|
69
|
+
def reopen(*args)
|
70
|
+
data = @api.post(command_path("reopen"))
|
71
|
+
end
|
72
|
+
|
73
|
+
def close(*args)
|
74
|
+
data = @api.post(command_path("close"))
|
75
|
+
end
|
76
|
+
|
77
|
+
def save
|
78
|
+
data = @api.post(command_path("edit"), { :title => self.title, :body => self.body })
|
79
|
+
end
|
80
|
+
|
81
|
+
%[add remove].each do |oper|
|
82
|
+
define_method("#{oper}_label") do |*labels|
|
83
|
+
labels.each do |label|
|
84
|
+
@api.post("#{prefix("label/#{oper}")}/#{label}/#{number}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def prefix(command)
|
91
|
+
"/issues/#{command}/#{repository.owner}/#{repository.name}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def command_path(command)
|
95
|
+
"#{prefix(command)}/#{number}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/octopi/key.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Key < Base
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
attr_reader :user
|
6
|
+
|
7
|
+
def initialize(api, data, user = nil)
|
8
|
+
super api, data
|
9
|
+
@user = user
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove!
|
13
|
+
result = @api.post "/user/key/remove", :id => id
|
14
|
+
keys = result["public_keys"].select { |k| k["title"] == title }
|
15
|
+
keys.empty?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Repository < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "repository", "repositories"
|
5
|
+
|
6
|
+
find_path "/repos/search/:query"
|
7
|
+
resource_path "/repos/show/:id"
|
8
|
+
|
9
|
+
def branches
|
10
|
+
Branch.find(self.owner, self.name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def tags
|
14
|
+
Tag.find(self.owner, self.name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def clone_url
|
18
|
+
#FIXME: Return "git@github.com:#{self.owner}/#{self.name}.git" if
|
19
|
+
#user's logged in and owns this repo.
|
20
|
+
"git://github.com/#{self.owner}/#{self.name}.git"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.find_by_user(user)
|
24
|
+
user = user.login if user.is_a? User
|
25
|
+
self.validate_args(user => :user)
|
26
|
+
find_plural(user, :resource)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find(*args)
|
30
|
+
api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
|
31
|
+
repo = args.pop
|
32
|
+
user = args.pop
|
33
|
+
|
34
|
+
user = user.login if user.is_a? User
|
35
|
+
if repo.is_a? Repository
|
36
|
+
repo = repo.name
|
37
|
+
user ||= repo.owner
|
38
|
+
end
|
39
|
+
|
40
|
+
self.validate_args(user => :user, repo => :repo)
|
41
|
+
super user, repo, api
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.find_all(*args)
|
45
|
+
# FIXME: This should be URI escaped, but have to check how the API
|
46
|
+
# handles escaped characters first.
|
47
|
+
super args.join(" ").gsub(/ /,'+')
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.open_issue(args)
|
51
|
+
Issue.open(args[:user], args[:repo], args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def open_issue(args)
|
55
|
+
Issue.open(self.owner, self, args, @api)
|
56
|
+
end
|
57
|
+
|
58
|
+
def commits(branch = "master")
|
59
|
+
Commit.find_all(self, :branch => branch)
|
60
|
+
end
|
61
|
+
|
62
|
+
def issues(state = "open")
|
63
|
+
Issue.find_all(self, :state => state)
|
64
|
+
end
|
65
|
+
|
66
|
+
def all_issues
|
67
|
+
Issue::STATES.map{|state| self.issues(state)}.flatten
|
68
|
+
end
|
69
|
+
|
70
|
+
def issue(number)
|
71
|
+
Issue.find(self, number)
|
72
|
+
end
|
73
|
+
|
74
|
+
def collaborators
|
75
|
+
property('collaborators', [self.owner,self.name].join('/')).values
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Octopi
|
2
|
+
module Resource
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.set_resource_name(base.name)
|
6
|
+
(@@resources||={})[base.resource_name(:singular)] = base
|
7
|
+
(@@resources||={})[base.resource_name(:plural)] = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.for(name)
|
11
|
+
@@resources[name]
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def set_resource_name(singular, plural = "#{singular}s")
|
16
|
+
@resource_name = {:singular => declassify(singular), :plural => declassify(plural)}
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource_name(key)
|
20
|
+
@resource_name[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_path(path)
|
24
|
+
(@path_spec||={})[:find] = path
|
25
|
+
end
|
26
|
+
|
27
|
+
def resource_path(path)
|
28
|
+
(@path_spec||={})[:resource] = path
|
29
|
+
end
|
30
|
+
|
31
|
+
def find(*args)
|
32
|
+
api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
|
33
|
+
args = args.join('/') if args.is_a? Array
|
34
|
+
result = api.find(path_for(:resource), @resource_name[:singular], args)
|
35
|
+
key = result.keys.first
|
36
|
+
|
37
|
+
if result[key].is_a? Array
|
38
|
+
result[key].map { |r| new(api, r) }
|
39
|
+
else
|
40
|
+
Resource.for(key).new(api, result[key])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_all(*s)
|
45
|
+
find_plural(s, :find)
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_plural(s, path, api = ANONYMOUS_API)
|
49
|
+
s = s.join('/') if s.is_a? Array
|
50
|
+
api.find_all(path_for(path), @resource_name[:plural], s).
|
51
|
+
map do |item|
|
52
|
+
payload = block_given? ? yield(item) : item
|
53
|
+
new(api, payload)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def declassify(s)
|
58
|
+
(s.split('::').last || '').downcase if s
|
59
|
+
end
|
60
|
+
|
61
|
+
def path_for(type)
|
62
|
+
@path_spec[type]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/octopi/tag.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Tag < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "tag"
|
5
|
+
|
6
|
+
resource_path "/repos/show/:id"
|
7
|
+
|
8
|
+
def self.find(user, repo)
|
9
|
+
user = user.login if user.is_a? User
|
10
|
+
repo = repo.name if repo.is_a? Repository
|
11
|
+
self.validate_args(user => :user, repo => :repo)
|
12
|
+
find_plural([user,repo,'tags'], :resource){
|
13
|
+
|i| {:name => i.first, :hash => i.last }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/octopi/user.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Octopi
|
2
|
+
class User < Base
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
find_path "/user/search/:query"
|
6
|
+
resource_path "/user/show/:id"
|
7
|
+
|
8
|
+
def self.find(username)
|
9
|
+
self.validate_args(username => :user)
|
10
|
+
super username
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find_all(username)
|
14
|
+
self.validate_args(username => :user)
|
15
|
+
super username
|
16
|
+
end
|
17
|
+
|
18
|
+
def repositories
|
19
|
+
Repository.find_by_user(login)
|
20
|
+
end
|
21
|
+
|
22
|
+
def repository(name)
|
23
|
+
self.class.validate_args(name => :repo)
|
24
|
+
Repository.find(login, name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_key(title, key)
|
28
|
+
raise APIError,
|
29
|
+
"To add a key, you must be authenticated" if @api.read_only?
|
30
|
+
|
31
|
+
result = @api.post("/user/key/add", :title => title, :key => key)
|
32
|
+
return if !result["public_keys"]
|
33
|
+
key_params = result["public_keys"].select { |k| k["title"] == title }
|
34
|
+
return if !key_params or key_params.empty?
|
35
|
+
Key.new(@api, key_params.first, self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def keys
|
39
|
+
raise APIError,
|
40
|
+
"To add a key, you must be authenticated" if @api.read_only?
|
41
|
+
|
42
|
+
result = @api.get("/user/keys")
|
43
|
+
return unless result and result["public_keys"]
|
44
|
+
result["public_keys"].inject([]) { |result, element| result << Key.new(@api, element) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# takes one param, deep that indicates if returns
|
48
|
+
# only the user login or an user object
|
49
|
+
%w[followers following].each do |method|
|
50
|
+
define_method(method) do
|
51
|
+
user_property(method, false)
|
52
|
+
end
|
53
|
+
define_method("#{method}!") do
|
54
|
+
user_property(method, true)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def user_property(property, deep)
|
59
|
+
users = []
|
60
|
+
property(property, login).each_pair do |k,v|
|
61
|
+
return v unless deep
|
62
|
+
|
63
|
+
v.each { |u| users << User.find(u) }
|
64
|
+
end
|
65
|
+
|
66
|
+
users
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/test/octopi_test.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class OctopiTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
|
6
|
+
# TODO: Those tests are obviously brittle. Need to stub/mock it.
|
7
|
+
|
8
|
+
def assert_find_all(cls, check_method, repo, user)
|
9
|
+
repo_method = cls.resource_name(:plural)
|
10
|
+
|
11
|
+
item1 = cls.find_all(user.login, repo.name).first
|
12
|
+
item2 = cls.find_all(repo).first
|
13
|
+
item3 = repo.send(repo_method).first
|
14
|
+
|
15
|
+
assert_equal item1.send(check_method), item2.send(check_method)
|
16
|
+
assert_equal item1.send(check_method), item3.send(check_method)
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@user = User.find("fcoury")
|
21
|
+
@repo = @user.repository("octopi")
|
22
|
+
@issue = @repo.issues.first
|
23
|
+
end
|
24
|
+
|
25
|
+
context Issue do
|
26
|
+
should "return the correct issue by number" do
|
27
|
+
assert_equal @issue.number, Issue.find(@repo, @issue.number).number
|
28
|
+
assert_equal @issue.number, Issue.find(@user, @repo, @issue.number).number
|
29
|
+
assert_equal @issue.number, Issue.find(@repo.owner, @repo.name, @issue.number).number
|
30
|
+
end
|
31
|
+
|
32
|
+
should "return the correct issue by using repo.issue number" do
|
33
|
+
assert_equal @issue.number, @repo.issue(@issue.number).number
|
34
|
+
end
|
35
|
+
|
36
|
+
should "fetch the same issue using different but equivalent find_all params" do
|
37
|
+
assert_find_all Issue, :number, @repo, @user
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context Commit do
|
42
|
+
should "fetch the same commit using different but equivalent find_all params" do
|
43
|
+
assert_find_all Commit, :id, @repo, @user
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: octopi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Felipe Coury
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-27 00:00:00 -03:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: felipe.coury@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- README.rdoc
|
27
|
+
- VERSION.yml
|
28
|
+
- lib/octopi/base.rb
|
29
|
+
- lib/octopi/blob.rb
|
30
|
+
- lib/octopi/branch.rb
|
31
|
+
- lib/octopi/commit.rb
|
32
|
+
- lib/octopi/error.rb
|
33
|
+
- lib/octopi/file_object.rb
|
34
|
+
- lib/octopi/issue.rb
|
35
|
+
- lib/octopi/key.rb
|
36
|
+
- lib/octopi/repository.rb
|
37
|
+
- lib/octopi/resource.rb
|
38
|
+
- lib/octopi/tag.rb
|
39
|
+
- lib/octopi/user.rb
|
40
|
+
- lib/octopi.rb
|
41
|
+
- test/octopi_test.rb
|
42
|
+
- test/test_helper.rb
|
43
|
+
- LICENSE
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://github.com/fcoury/octopi
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --inline-source
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: octopi
|
69
|
+
rubygems_version: 1.3.2
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: A Ruby interface to GitHub API v2
|
73
|
+
test_files: []
|
74
|
+
|