octopi 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +2 -1
  2. data/.yardoc +0 -0
  3. data/README.rdoc +16 -41
  4. data/Rakefile +9 -0
  5. data/VERSION.yml +2 -2
  6. data/examples/overall.rb +1 -1
  7. data/lib/ext/hash_ext.rb +5 -0
  8. data/lib/ext/string_ext.rb +5 -0
  9. data/lib/octopi.rb +101 -202
  10. data/lib/octopi/api.rb +209 -0
  11. data/lib/octopi/base.rb +42 -38
  12. data/lib/octopi/blob.rb +12 -8
  13. data/lib/octopi/branch.rb +20 -7
  14. data/lib/octopi/branch_set.rb +11 -0
  15. data/lib/octopi/comment.rb +20 -0
  16. data/lib/octopi/commit.rb +39 -35
  17. data/lib/octopi/error.rb +17 -5
  18. data/lib/octopi/file_object.rb +6 -5
  19. data/lib/octopi/gist.rb +28 -0
  20. data/lib/octopi/issue.rb +49 -40
  21. data/lib/octopi/issue_comment.rb +7 -0
  22. data/lib/octopi/issue_set.rb +21 -0
  23. data/lib/octopi/key.rb +14 -7
  24. data/lib/octopi/key_set.rb +14 -0
  25. data/lib/octopi/plan.rb +5 -0
  26. data/lib/octopi/repository.rb +66 -45
  27. data/lib/octopi/repository_set.rb +9 -0
  28. data/lib/octopi/resource.rb +11 -16
  29. data/lib/octopi/self.rb +33 -0
  30. data/lib/octopi/tag.rb +12 -6
  31. data/lib/octopi/user.rb +62 -38
  32. data/octopi.gemspec +43 -12
  33. data/test/api_test.rb +58 -0
  34. data/test/authenticated_test.rb +39 -0
  35. data/test/blob_test.rb +23 -0
  36. data/test/branch_test.rb +20 -0
  37. data/test/commit_test.rb +82 -0
  38. data/test/file_object_test.rb +39 -0
  39. data/test/gist_test.rb +16 -0
  40. data/test/issue_comment.rb +19 -0
  41. data/test/issue_set_test.rb +33 -0
  42. data/test/issue_test.rb +120 -0
  43. data/test/key_set_test.rb +29 -0
  44. data/test/key_test.rb +35 -0
  45. data/test/repository_set_test.rb +23 -0
  46. data/test/repository_test.rb +141 -0
  47. data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
  48. data/test/tag_test.rb +20 -0
  49. data/test/test_helper.rb +236 -0
  50. data/test/user_test.rb +92 -0
  51. metadata +54 -12
  52. data/examples/github.yml.example +0 -14
  53. data/test/octopi_test.rb +0 -46
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  examples/github.yml
2
- rdoc/
2
+ doc/
3
3
  pkg/
4
+ contrib/nothingspecial.rb
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 |g|
19
- repo = g.repository("api-labrat")
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 |g|
30
- repo = g.repository("api-labrat")
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 a YAML file with authentication information:</b>
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 1
4
- :patch: 0
3
+ :minor: 2
4
+ :patch: 1
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.details.modified.each {|m| puts "#{m['filename']} DIFF: #{m['diff']}" }
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")
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def method_missing(method, *args)
3
+ self[method] || self[method.to_s]
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def camel_case
3
+ self.gsub(/(^|_)(.)/) { $2.upcase }
4
+ end
5
+ end
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
- module Octopi
7
- def authenticated(*args, &block)
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
- yield api
19
- end
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
- def authenticated_with(*args, &block)
22
- opts = args.last.is_a?(Hash) ? args.last : {}
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 read_gitconfig
42
- config = {}
43
- group = nil
44
- File.foreach("#{ENV['HOME']}/.gitconfig") do |line|
45
- line.strip!
46
- if line[0] != ?# and line =~ /\S/
47
- if line =~ /^\[(.*)\]$/
48
- group = $1
49
- else
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
- class Api
60
- CONTENT_TYPE = {
61
- 'yaml' => 'application/x-yaml',
62
- 'json' => 'application/json',
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 login
75
- @login = login
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
- end
81
-
82
- def read_only?
83
- read_only
84
- end
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
- def open_issue(user, repo, params)
99
- Issue.open(user, repo, params, self)
100
- end
56
+ trace("=> Trace on: #{options[:trace]}")
101
57
 
102
- def repository(name)
103
- repo = Repository.find(user, name, self)
104
- repo.api = self
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
- def commits(repo,opts={})
110
- branch = opts[:branch] || "master"
111
- commits = Commit.find_all(repo, branch, self)
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
- 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
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
- def find(path, result_key, resource_id)
121
- get(path, { :id => resource_id })
122
- end
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
- private
174
- def submit(path, params = {}, format = "yaml", &block)
175
- params.each_pair do |k,v|
176
- if path =~ /:#{k.to_s}/
177
- params.delete(k)
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
- begin
185
- resp = yield(path, query.merge(params), format)
186
- rescue Net::HTTPBadResponse
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 @trace_level
191
- case @trace_level
192
- when "curl"
193
- query_trace = []
194
- query.each_pair { |k,v| query_trace << "-F '#{k}=#{v}'" }
195
- puts "===== [curl version]"
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
- raise RetryableAPIError, resp.code.to_i if RETRYABLE_STATUS.include? resp.code.to_i
201
- raise APIError,
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
- class AuthApi < Api
222
- include HTTParty
223
- base_uri "https://github.com/api/v2"
224
- end
225
-
226
- class AnonymousApi < Api
227
- include HTTParty
228
- base_uri "http://github.com/api/v2"
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
- ANONYMOUS_API = AnonymousApi.new
232
-
233
- %w{error base resource user tag repository issue file_object blob key commit branch}.
234
- each{|f| require "#{File.dirname(__FILE__)}/octopi/#{f}"}
235
-
130
+ def trace(text)
131
+ if Api.api.trace_level
132
+ puts "text"
133
+ end
134
+ end
236
135
  end