octopi 0.0.9

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