octopi 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|