octopi 0.1.0 → 0.2.1
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/.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
|