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.
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