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 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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 9
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'octopi'
8
+
9
+ class Test::Unit::TestCase
10
+ end
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
+