octopi 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/.yardoc +0 -0
- data/README.rdoc +16 -41
- data/Rakefile +9 -0
- data/VERSION.yml +2 -2
- data/examples/overall.rb +1 -1
- data/lib/ext/hash_ext.rb +5 -0
- data/lib/ext/string_ext.rb +5 -0
- data/lib/octopi.rb +101 -202
- data/lib/octopi/api.rb +209 -0
- data/lib/octopi/base.rb +42 -38
- data/lib/octopi/blob.rb +12 -8
- data/lib/octopi/branch.rb +20 -7
- data/lib/octopi/branch_set.rb +11 -0
- data/lib/octopi/comment.rb +20 -0
- data/lib/octopi/commit.rb +39 -35
- data/lib/octopi/error.rb +17 -5
- data/lib/octopi/file_object.rb +6 -5
- data/lib/octopi/gist.rb +28 -0
- data/lib/octopi/issue.rb +49 -40
- data/lib/octopi/issue_comment.rb +7 -0
- data/lib/octopi/issue_set.rb +21 -0
- data/lib/octopi/key.rb +14 -7
- data/lib/octopi/key_set.rb +14 -0
- data/lib/octopi/plan.rb +5 -0
- data/lib/octopi/repository.rb +66 -45
- data/lib/octopi/repository_set.rb +9 -0
- data/lib/octopi/resource.rb +11 -16
- data/lib/octopi/self.rb +33 -0
- data/lib/octopi/tag.rb +12 -6
- data/lib/octopi/user.rb +62 -38
- data/octopi.gemspec +43 -12
- data/test/api_test.rb +58 -0
- data/test/authenticated_test.rb +39 -0
- data/test/blob_test.rb +23 -0
- data/test/branch_test.rb +20 -0
- data/test/commit_test.rb +82 -0
- data/test/file_object_test.rb +39 -0
- data/test/gist_test.rb +16 -0
- data/test/issue_comment.rb +19 -0
- data/test/issue_set_test.rb +33 -0
- data/test/issue_test.rb +120 -0
- data/test/key_set_test.rb +29 -0
- data/test/key_test.rb +35 -0
- data/test/repository_set_test.rb +23 -0
- data/test/repository_test.rb +141 -0
- data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
- data/test/tag_test.rb +20 -0
- data/test/test_helper.rb +236 -0
- data/test/user_test.rb +92 -0
- metadata +54 -12
- data/examples/github.yml.example +0 -14
- data/test/octopi_test.rb +0 -46
data/.gitignore
CHANGED
data/.yardoc
ADDED
Binary file
|
data/README.rdoc
CHANGED
@@ -15,9 +15,8 @@ http://twitter.com/octopi_gem
|
|
15
15
|
|
16
16
|
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:
|
17
17
|
|
18
|
-
authenticated do
|
19
|
-
repo =
|
20
|
-
(...)
|
18
|
+
authenticated do
|
19
|
+
repo = Repository.find(:name => "api-labrat", :user => "fcoury")
|
21
20
|
end
|
22
21
|
|
23
22
|
=== Explicit authentication
|
@@ -26,14 +25,23 @@ Sometimes, you may not want to get authentication data from <tt>~/.gitconfig</tt
|
|
26
25
|
|
27
26
|
<b>1. Providing login and token inline:</b>
|
28
27
|
|
29
|
-
authenticated_with "mylogin", "mytoken" do
|
30
|
-
repo =
|
28
|
+
authenticated_with "mylogin", "mytoken" do
|
29
|
+
repo = Repository.find(:name => "api-labrat", :user => "fcoury")
|
31
30
|
issue = repo.open_issue :title => "Sample issue",
|
32
31
|
:body => "This issue was opened using GitHub API and Octopi"
|
33
32
|
puts issue.number
|
34
33
|
end
|
35
|
-
|
36
|
-
<b>2. Providing
|
34
|
+
|
35
|
+
<b>2. Providing login and password inline:</b>
|
36
|
+
|
37
|
+
authenticated_with "mylogin", "password" do
|
38
|
+
repo = Repository.find(:name => "api-labrat", :user => "fcoury")
|
39
|
+
issue = repo.open_issue :title => "Sample issue",
|
40
|
+
:body => "This issue was opened using GitHub API and Octopi"
|
41
|
+
puts issue.number
|
42
|
+
end
|
43
|
+
|
44
|
+
<b>3. Providing a YAML file with authentication information:</b>
|
37
45
|
|
38
46
|
Use the following format:
|
39
47
|
|
@@ -116,40 +124,6 @@ Single commit information:
|
|
116
124
|
puts "Diff:"
|
117
125
|
first_commit.details.modified.each {|m| puts "#{m['filename']} DIFF: #{m['diff']}" }
|
118
126
|
|
119
|
-
== Tracing
|
120
|
-
|
121
|
-
=== Levels
|
122
|
-
|
123
|
-
You can can use tracing to enable better debugging output when something goes wrong. There are 3 tracing levels:
|
124
|
-
|
125
|
-
* false (default) - no tracing
|
126
|
-
* true - will output each GET and POST calls, along with URL and params
|
127
|
-
* curl - same as true, but additionally outputs the curl command to replicate the issue
|
128
|
-
|
129
|
-
If you choose curl tracing, the curl command equivalent to each command sent to GitHub will be output to the stdout, like this example:
|
130
|
-
|
131
|
-
=> Trace on: curl
|
132
|
-
POST: /issues/open/webbynode/api-labrat params: body=This issue was opened using GitHub API and Octopi, title=Sample issue
|
133
|
-
===== curl version
|
134
|
-
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
|
135
|
-
==================
|
136
|
-
|
137
|
-
=== Enabling
|
138
|
-
|
139
|
-
Tracing can be enabled in different ways, depending on the API feature you're using:
|
140
|
-
|
141
|
-
<b>Anonymous (this will be improved later):</b>
|
142
|
-
|
143
|
-
ANONYMOUS_API.trace_level = "trace-level"
|
144
|
-
|
145
|
-
<b>Seamless authenticated</b>
|
146
|
-
|
147
|
-
authenticated :trace => "trace-level" do |g|; ...; end
|
148
|
-
|
149
|
-
<b>Explicitly authenticated</b>
|
150
|
-
|
151
|
-
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.
|
152
|
-
|
153
127
|
== Author
|
154
128
|
|
155
129
|
* Felipe Coury - http://felipecoury.com
|
@@ -159,6 +133,7 @@ Current version of explicit authentication requires a :config param to a YAML fi
|
|
159
133
|
|
160
134
|
In alphabetical order:
|
161
135
|
|
136
|
+
* Ryan Bigg - http://frozenplague.net
|
162
137
|
* Brandon Calloway - http://github.com/bcalloway
|
163
138
|
* runpaint - http://github.com/runpaint
|
164
139
|
|
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'jeweler'
|
@@ -10,9 +11,17 @@ begin
|
|
10
11
|
gem.homepage = "http://github.com/fcoury/octopi"
|
11
12
|
gem.authors = ["Felipe Coury"]
|
12
13
|
gem.rubyforge_project = "octopi"
|
14
|
+
gem.add_dependency('nokogiri', '>= 1.3.1')
|
15
|
+
gem.add_dependency('httparty', '>= 0.4.5')
|
16
|
+
gem.files.exclude 'test/**/*'
|
17
|
+
gem.files.exclude 'test*'
|
18
|
+
gem.files.exclude 'doc/**/*'
|
19
|
+
gem.files.exclude 'examples/**/*'
|
20
|
+
|
13
21
|
|
14
22
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
23
|
end
|
24
|
+
Jeweler::GemcutterTasks.new
|
16
25
|
rescue LoadError
|
17
26
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
27
|
end
|
data/VERSION.yml
CHANGED
data/examples/overall.rb
CHANGED
@@ -36,7 +36,7 @@ puts "Commit: #{commit.id} - #{commit.message} - by #{commit.author['name']}"
|
|
36
36
|
# single commit information
|
37
37
|
# details is the same as: Commit.find(commit)
|
38
38
|
puts "Diff:"
|
39
|
-
commit.
|
39
|
+
commit.modified.each {|m| puts "#{m['filename']} DIFF: #{m['diff']}" }
|
40
40
|
|
41
41
|
# repository search
|
42
42
|
repos = Repository.find_all("ruby", "git")
|
data/lib/ext/hash_ext.rb
ADDED
data/lib/octopi.rb
CHANGED
@@ -1,236 +1,135 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
|
2
3
|
require 'httparty'
|
4
|
+
require 'mechanize'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'api_cache'
|
7
|
+
|
3
8
|
require 'yaml'
|
4
9
|
require 'pp'
|
5
10
|
|
6
|
-
|
7
|
-
|
8
|
-
opts = args.last.is_a?(Hash) ? args.last : {}
|
9
|
-
config = read_gitconfig
|
10
|
-
login = config["github"]["user"]
|
11
|
-
token = config["github"]["token"]
|
12
|
-
|
13
|
-
api = AuthApi.new(login, token)
|
14
|
-
api.trace_level = opts[:trace]
|
15
|
-
|
16
|
-
puts "=> Trace on: #{api.trace_level}" if api.trace_level
|
11
|
+
# Core extension stuff
|
12
|
+
Dir[File.join(File.dirname(__FILE__), "ext/*.rb")].each { |f| require f }
|
17
13
|
|
18
|
-
|
19
|
-
|
14
|
+
# Octopi stuff
|
15
|
+
Dir[File.join(File.dirname(__FILE__), "octopi/*.rb")].each { |f| require f }
|
16
|
+
|
17
|
+
# Include this into your app so you can access the child classes easier.
|
18
|
+
# This is the root of all things Octopi.
|
19
|
+
module Octopi
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
if opts[:config]
|
24
|
-
config = File.open(opts[:config]) { |yf| YAML::load(yf) }
|
25
|
-
raise "Missing config #{opts[:config]}" unless config
|
26
|
-
|
27
|
-
login = config["login"]
|
28
|
-
token = config["token"]
|
29
|
-
trace = config["trace"]
|
30
|
-
else
|
31
|
-
login, token = *args
|
32
|
-
end
|
33
|
-
|
34
|
-
puts "=> Trace on: #{trace}" if trace
|
35
|
-
|
36
|
-
api = AuthApi.new(login, token)
|
37
|
-
api.trace_level = trace if trace
|
38
|
-
yield api
|
39
|
-
end
|
21
|
+
# The authenticated methods are all very similar.
|
22
|
+
# TODO: Find a way to merge them into something... better.
|
40
23
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
key, value = line.split("=")
|
51
|
-
value ||= ''
|
52
|
-
(config[group]||={})[key.strip] = value.strip
|
53
|
-
end
|
24
|
+
def authenticated(options={}, &block)
|
25
|
+
begin
|
26
|
+
config = config = File.open(options[:config]) { |yf| YAML::load(yf) } if options[:config]
|
27
|
+
config = read_gitconfig
|
28
|
+
options[:login] = config["github"]["user"]
|
29
|
+
options[:token] = config["github"]["token"]
|
30
|
+
|
31
|
+
authenticated_with(options) do
|
32
|
+
yield
|
54
33
|
end
|
34
|
+
ensure
|
35
|
+
# Reset authenticated so if we were to do an anonymous call it would Just Work(tm)
|
36
|
+
Api.authenticated = false
|
37
|
+
Api.api = AnonymousApi.instance
|
55
38
|
end
|
56
|
-
config
|
57
39
|
end
|
58
40
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
'xml' => 'application/sml'
|
64
|
-
}
|
65
|
-
RETRYABLE_STATUS = [403]
|
66
|
-
MAX_RETRIES = 10
|
67
|
-
|
68
|
-
attr_accessor :format, :login, :token, :trace_level, :read_only
|
69
|
-
|
70
|
-
def initialize(login = nil, token = nil, format = "yaml")
|
71
|
-
@format = format
|
72
|
-
@read_only = true
|
41
|
+
def authenticated_with(options, &block)
|
42
|
+
begin
|
43
|
+
|
44
|
+
Api.api.trace_level = options[:trace] if options[:trace]
|
73
45
|
|
74
|
-
if
|
75
|
-
|
76
|
-
@token = token
|
77
|
-
@read_only = false
|
78
|
-
self.class.default_params :login => login, :token => token
|
46
|
+
if options[:token].nil? && !options[:password].nil?
|
47
|
+
options[:token] = grab_token(options[:login], options[:password])
|
79
48
|
end
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
{:keys => 'public_keys', :emails => 'emails'}.each_pair do |action, key|
|
87
|
-
define_method("#{action}") do
|
88
|
-
get("/user/#{action}")[key]
|
49
|
+
begin
|
50
|
+
User.find(options[:login])
|
51
|
+
# If the user cannot see themselves then they are not logged in, tell them so
|
52
|
+
rescue Octopi::NotFound
|
53
|
+
raise Octopi::InvalidLogin
|
89
54
|
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def user
|
93
|
-
user_data = get("/user/show/#{login}")
|
94
|
-
raise "Unexpected response for user command" unless user_data and user_data['user']
|
95
|
-
User.new(self, user_data['user'])
|
96
|
-
end
|
97
55
|
|
98
|
-
|
99
|
-
Issue.open(user, repo, params, self)
|
100
|
-
end
|
56
|
+
trace("=> Trace on: #{options[:trace]}")
|
101
57
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
repo
|
106
|
-
end
|
107
|
-
alias_method :repo, :repository
|
58
|
+
Api.api = AuthApi.instance
|
59
|
+
Api.api.login = options[:login]
|
60
|
+
Api.api.token = options[:token]
|
108
61
|
|
109
|
-
|
110
|
-
|
111
|
-
|
62
|
+
yield
|
63
|
+
ensure
|
64
|
+
# Reset authenticated so if we were to do an anonymous call it would Just Work(tm)
|
65
|
+
Api.authenticated = false
|
66
|
+
Api.api = AnonymousApi.instance
|
112
67
|
end
|
68
|
+
end
|
113
69
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def grab_token(username, password)
|
73
|
+
a = WWW::Mechanize.new { |agent|
|
74
|
+
# Fake out the agent
|
75
|
+
agent.user_agent_alias = 'Mac Safari'
|
76
|
+
}
|
119
77
|
|
120
|
-
|
121
|
-
|
122
|
-
|
78
|
+
# Login with the provided
|
79
|
+
a.get('http://github.com/login') do |page|
|
80
|
+
user_page = page.form_with(:action => '/session') do |login|
|
81
|
+
login.login = username
|
82
|
+
login.password = password
|
83
|
+
end.submit
|
123
84
|
|
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
|
-
sleep 6
|
144
|
-
retry
|
145
|
-
else
|
146
|
-
raise APIError, "GitHub returned status #{e.code}, despite" +
|
147
|
-
" repeating the request #{MAX_RETRIES} times. Giving up."
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def post(path, params = {}, format = "yaml")
|
153
|
-
@@retries = 0
|
154
|
-
begin
|
155
|
-
trace "POST", "/#{format}#{path}", params
|
156
|
-
submit(path, params, format) do |path, params, format|
|
157
|
-
resp = self.class.post "/#{format}#{path}", :body => params
|
158
|
-
resp
|
159
|
-
end
|
160
|
-
rescue RetryableAPIError => e
|
161
|
-
if @@retries < MAX_RETRIES
|
162
|
-
$stderr.puts e.message
|
163
|
-
@@retries += 1
|
164
|
-
sleep 6
|
165
|
-
retry
|
166
|
-
else
|
167
|
-
raise APIError, "GitHub returned status #{e.code}, despite" +
|
168
|
-
" repeating the request #{MAX_RETRIES} times. Giving up."
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
85
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
path = path.gsub(":#{k.to_s}", v)
|
179
|
-
end
|
86
|
+
if Api.api.trace_level
|
87
|
+
File.open("got.html", "w+") do |f|
|
88
|
+
f.write user_page.body
|
89
|
+
end
|
90
|
+
`open got.html`
|
180
91
|
end
|
181
|
-
query = login ? { :login => login, :token => token } : {}
|
182
|
-
query.merge!(params)
|
183
92
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
raise RetryableAPIError
|
188
|
-
end
|
93
|
+
body = Nokogiri::HTML(user_page.body)
|
94
|
+
error = body.xpath("//div[@class='error_box']").text
|
95
|
+
raise error if error != ""
|
189
96
|
|
190
|
-
if
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
puts "curl #{query_trace.join(" ")} #{self.class.base_uri}/#{format}#{path}"
|
197
|
-
puts "===================="
|
97
|
+
# Should be clear to go if there is no errors.
|
98
|
+
link = user_page.link_with(:text => "account")
|
99
|
+
@account_page = a.click(link)
|
100
|
+
if Api.api.trace_level
|
101
|
+
File.open("account.html", "w+") do |f|
|
102
|
+
f.write @account_page.body
|
198
103
|
end
|
104
|
+
`open account.html`
|
199
105
|
end
|
200
|
-
|
201
|
-
|
202
|
-
"GitHub returned status #{resp.code}" unless resp.code.to_i == 200
|
203
|
-
# FIXME: This fails for showing raw Git data because that call returns
|
204
|
-
# text/html as the content type. This issue has been reported.
|
205
|
-
ctype = resp.headers['content-type'].first
|
206
|
-
raise FormatError, [ctype, format] unless
|
207
|
-
ctype.match(/^#{CONTENT_TYPE[format]};/)
|
208
|
-
if format == 'yaml' && resp['error']
|
209
|
-
raise APIError, resp['error'].first['error']
|
210
|
-
end
|
211
|
-
resp
|
212
|
-
end
|
213
|
-
|
214
|
-
def trace(oper, url, params)
|
215
|
-
return unless trace_level
|
216
|
-
par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params and !params.empty?
|
217
|
-
puts "#{oper}: #{url}#{par_str}"
|
106
|
+
|
107
|
+
return Nokogiri::HTML(@account_page.body).xpath("//p").xpath("strong")[1].text
|
218
108
|
end
|
219
109
|
end
|
220
110
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
111
|
+
|
112
|
+
def read_gitconfig
|
113
|
+
config = {}
|
114
|
+
group = nil
|
115
|
+
File.foreach("#{ENV['HOME']}/.gitconfig") do |line|
|
116
|
+
line.strip!
|
117
|
+
if line[0] != ?# && line =~ /\S/
|
118
|
+
if line =~ /^\[(.*)\]$/
|
119
|
+
group = $1
|
120
|
+
config[group] ||= {}
|
121
|
+
else
|
122
|
+
key, value = line.split("=").map { |v| v.strip }
|
123
|
+
config[group][key] = value
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
config
|
229
128
|
end
|
230
129
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
130
|
+
def trace(text)
|
131
|
+
if Api.api.trace_level
|
132
|
+
puts "text"
|
133
|
+
end
|
134
|
+
end
|
236
135
|
end
|