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/lib/octopi/api.rb ADDED
@@ -0,0 +1,209 @@
1
+ require 'singleton'
2
+ require File.join(File.dirname(__FILE__), "self")
3
+ module Octopi
4
+ # Dummy class, so AnonymousApi and AuthApi have somewhere to inherit from
5
+ class Api
6
+ include Self
7
+ attr_accessor :format, :login, :token, :trace_level, :read_only
8
+ end
9
+
10
+ # Used for accessing the Github API anonymously
11
+ class AnonymousApi < Api
12
+ include HTTParty
13
+ include Singleton
14
+ base_uri "http://github.com/api/v2"
15
+
16
+ def read_only?
17
+ true
18
+ end
19
+
20
+ def auth_parameters
21
+ { }
22
+ end
23
+ end
24
+
25
+ class AuthApi < Api
26
+ include HTTParty
27
+ include Singleton
28
+ base_uri "https://github.com/api/v2"
29
+
30
+ def read_only?
31
+ false
32
+ end
33
+
34
+ def auth_parameters
35
+ { :login => Api.me.login, :token => Api.me.token }
36
+ end
37
+ end
38
+
39
+ # This is the real API class.
40
+ #
41
+ # API requests are limited to 60 per minute.
42
+ #
43
+ # Sets up basic methods for accessing the API.
44
+ class Api
45
+ @@api = Octopi::AnonymousApi.instance
46
+ @@authenticated = false
47
+
48
+ include Singleton
49
+ CONTENT_TYPE = {
50
+ 'yaml' => ['application/x-yaml', 'text/yaml', 'text/x-yaml', 'application/yaml'],
51
+ 'json' => 'application/json',
52
+ 'xml' => 'application/xml',
53
+ # Unexpectedly, Github returns resources such as blobs as text/html!
54
+ # Thus, plain == text/html.
55
+ 'plain' => ['text/plain', 'text/html']
56
+ }
57
+ RETRYABLE_STATUS = [403]
58
+ MAX_RETRIES = 10
59
+ # Would be nice if cattr_accessor was available, oh well.
60
+
61
+ # We use this to check if we use the auth or anonymous api
62
+ def self.authenticated
63
+ @@authenticated
64
+ end
65
+
66
+ # We set this to true when the user has auth'd.
67
+ def self.authenticated=(value)
68
+ @@authenticated = value
69
+ end
70
+
71
+ # The API we're using
72
+ def self.api
73
+ @@api
74
+ end
75
+
76
+ class << self
77
+ alias_method :me, :api
78
+ end
79
+
80
+ # set the API we're using
81
+ def self.api=(value)
82
+ @@api = value
83
+ end
84
+
85
+
86
+ def user
87
+ user_data = get("/user/show/#{login}")
88
+ raise "Unexpected response for user command" unless user_data and user_data['user']
89
+ User.new(user_data['user'])
90
+ end
91
+
92
+ def save(resource_path, data)
93
+ traslate resource_path, data
94
+ #still can't figure out on what format values are expected
95
+ post("#{resource_path}", { :query => data })
96
+ end
97
+
98
+
99
+ def find(path, result_key, resource_id, klass=nil, cache=true)
100
+ result = get(path, { :id => resource_id, :cache => cache }, klass)
101
+ result
102
+ end
103
+
104
+
105
+ def find_all(path, result_key, query, klass=nil, cache=true)
106
+ { :query => query, :id => query, :cache => cache }
107
+ result = get(path, { :query => query, :id => query, :cache => cache }, klass)
108
+ result[result_key]
109
+ end
110
+
111
+ def get_raw(path, params, klass=nil)
112
+ get(path, params, klass, 'plain')
113
+ end
114
+
115
+ def get(path, params = {}, klass=nil, format = :yaml)
116
+ @@retries = 0
117
+ begin
118
+ submit(path, params, klass, format) do |path, params, format, query|
119
+ self.class.get "/#{format}#{path}", { :format => format, :query => query }
120
+ end
121
+ rescue RetryableAPIError => e
122
+ if @@retries < MAX_RETRIES
123
+ $stderr.puts e.message
124
+ @@retries += 1
125
+ sleep 6
126
+ retry
127
+ else
128
+ raise APIError, "GitHub returned status #{e.code}, despite" +
129
+ " repeating the request #{MAX_RETRIES} times. Giving up."
130
+ end
131
+ end
132
+ end
133
+
134
+ def post(path, params = {}, klass=nil, format = :yaml)
135
+ @@retries = 0
136
+ begin
137
+ trace "POST", "/#{format}#{path}", params
138
+ submit(path, params, klass, format) do |path, params, format, query|
139
+ resp = self.class.post "/#{format}#{path}", { :body => params, :format => format, :query => query }
140
+ resp
141
+ end
142
+ rescue RetryableAPIError => e
143
+ if @@retries < MAX_RETRIES
144
+ $stderr.puts e.message
145
+ @@retries += 1
146
+ sleep 6
147
+ retry
148
+ else
149
+ raise APIError, "GitHub returned status #{e.code}, despite" +
150
+ " repeating the request #{MAX_RETRIES} times. Giving up."
151
+ end
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def method_missing(method, *args)
158
+ api.send(method, *args)
159
+ end
160
+
161
+ def submit(path, params = {}, klass=nil, format = :yaml, &block)
162
+ # Ergh. Ugly way to do this. Find a better one!
163
+ cache = params.delete(:cache)
164
+ cache = true if cache.nil?
165
+ params.each_pair do |k,v|
166
+ if path =~ /:#{k.to_s}/
167
+ params.delete(k)
168
+ path = path.gsub(":#{k.to_s}", v)
169
+ end
170
+ end
171
+ begin
172
+ key = "#{Api.api.class.to_s}:#{path}"
173
+ resp = if cache
174
+ APICache.get(key, :cache => 61) do
175
+ yield(path, params, format, auth_parameters)
176
+ end
177
+ else
178
+ yield(path, params, format, auth_parameters)
179
+ end
180
+ rescue Net::HTTPBadResponse
181
+ raise RetryableAPIError
182
+ end
183
+
184
+ raise RetryableAPIError, resp.code.to_i if RETRYABLE_STATUS.include? resp.code.to_i
185
+ # puts resp.code.inspect
186
+ raise NotFound, klass || self.class if resp.code.to_i == 404
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
+
192
+ # It happens, in tests.
193
+ return resp if resp.headers.empty?
194
+ ctype = resp.headers['content-type'].first.split(";").first
195
+ raise FormatError, [ctype, format] unless CONTENT_TYPE[format.to_s].include?(ctype)
196
+ if format == 'yaml' && resp['error']
197
+ raise APIError, resp['error']
198
+ end
199
+ resp
200
+ end
201
+
202
+ def trace(oper, url, params)
203
+ return unless trace_level
204
+ par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params && !params.empty?
205
+ puts "#{oper}: #{url}#{par_str}"
206
+ end
207
+
208
+ end
209
+ end
data/lib/octopi/base.rb CHANGED
@@ -1,8 +1,3 @@
1
- class String
2
- def camel_case
3
- self.gsub(/(^|_)(.)/) { $2.upcase }
4
- end
5
- end
6
1
  module Octopi
7
2
  class Base
8
3
  VALID = {
@@ -14,9 +9,6 @@ module Octopi
14
9
  :user => {
15
10
  :pat => /^[A-Za-z0-9_\.-]+$/,
16
11
  :msg => "%s is an invalid username"},
17
- :file => {
18
- :pat => /^[^ \/]+$/,
19
- :msg => "%s is an invalid filename"},
20
12
  :sha => {
21
13
  :pat => /^[a-f0-9]{40}$/,
22
14
  :msg => "%s is an invalid SHA hash"},
@@ -29,62 +21,70 @@ module Octopi
29
21
 
30
22
  attr_accessor :api
31
23
 
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
24
+ def initialize(attributes={})
25
+ # Useful for finding out what attr_accessor needs for classes
26
+ # puts caller.first.inspect
27
+ # puts "#{self.class.inspect} #{attributes.keys.map { |s| s.to_sym }.inspect}"
28
+ attributes.each do |key, value|
29
+ raise "no attr_accessor set for #{key} on #{self.class}" if !respond_to?("#{key}=")
30
+ self.send("#{key}=", value)
31
+ end
32
+ end
33
+
34
+ def error=(error)
35
+ if /\w+ not found/.match(error)
36
+ raise NotFound, self.class
52
37
  end
53
38
  end
54
39
 
55
40
  def property(p, v)
56
41
  path = "#{self.class.path_for(:resource)}/#{p}"
57
- @api.find(path, self.class.resource_name(:singular), v)
42
+ Api.api.find(path, self.class.resource_name(:singular), v)
58
43
  end
59
44
 
60
45
  def save
61
46
  hash = {}
62
47
  @keys.each { |k| hash[k] = send(k) }
63
- @api.save(self.path_for(:resource), hash)
48
+ Api.api.save(self.path_for(:resource), hash)
64
49
  end
65
50
 
66
51
  private
52
+
53
+ def self.gather_name(options)
54
+ options[:repository] || options[:repo] || options[:name]
55
+ end
56
+
57
+ def self.gather_details(options)
58
+ repo = self.gather_name(options)
59
+ repo = Repository.find(:user => options[:user], :name => repo) if !repo.is_a?(Repository)
60
+ user = repo.owner.to_s
61
+ user ||= options[:user].to_s
62
+ branch = options[:branch] || "master"
63
+ self.validate_args(user => :user, repo.name => :repo)
64
+ [user, repo, branch, options[:sha]].compact
65
+ end
66
+
67
67
  def self.extract_user_repository(*args)
68
- opts = args.last.is_a?(Hash) ? args.pop : {}
69
- if opts.empty?
68
+ options = args.last.is_a?(Hash) ? args.pop : {}
69
+ if options.empty?
70
70
  if args.length > 1
71
71
  repo, user = *args
72
72
  else
73
73
  repo = args.pop
74
74
  end
75
75
  else
76
- opts[:repo] = opts[:repository] if opts[:repository]
77
- repo = args.pop || opts[:repo]
78
- user = opts[:user]
76
+ options[:repo] = options[:repository] if options[:repository]
77
+ repo = args.pop || options[:repo]
78
+ user = options[:user]
79
79
  end
80
80
 
81
81
  user = repo.owner if repo.is_a? Repository
82
82
 
83
- if repo.is_a?(String) and !user
83
+ if repo.is_a?(String) && !user
84
84
  raise "Need user argument when repository is identified by name"
85
85
  end
86
86
  ret = extract_names(user, repo)
87
- ret << opts
87
+ ret << options
88
88
  ret
89
89
  end
90
90
 
@@ -95,7 +95,11 @@ module Octopi
95
95
  v
96
96
  end
97
97
  end
98
-
98
+
99
+ def self.ensure_hash(spec)
100
+ raise ArgumentMustBeHash, "find takes a hash of options as a solitary argument" if !spec.is_a?(Hash)
101
+ end
102
+
99
103
  def self.validate_args(spec)
100
104
  m = caller[0].match(/\/([a-z0-9_]+)\.rb:\d+:in `([a-z_0-9]+)'/)
101
105
  meth = m ? "#{m[1].camel_case}.#{m[2]}" : 'method'
data/lib/octopi/blob.rb CHANGED
@@ -1,20 +1,24 @@
1
+ require File.join(File.dirname(__FILE__), "resource")
1
2
  module Octopi
2
3
  class Blob < Base
4
+ attr_accessor :text, :data, :name, :sha, :size, :mode, :mime_type
3
5
  include Resource
4
6
  set_resource_name "blob"
5
7
 
6
8
  resource_path "/blob/show/:id"
7
9
 
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)
10
+ def self.find(options={})
11
+ ensure_hash(options)
12
+ user, repo = gather_details(options)
13
+ sha = options[:sha]
14
+ path = options[:path]
15
+
16
+ self.validate_args(sha => :sha, user => :user)
17
+
12
18
  if path
13
- super [user,repo,sha,path]
19
+ super [user, repo, sha, path]
14
20
  else
15
- blob = ANONYMOUS_API.get_raw(path_for(:resource),
16
- {:id => [user,repo,sha].join('/')})
17
- new(ANONYMOUS_API, {:text => blob})
21
+ Api.api.get_raw(path_for(:resource), {:id => [user, repo, sha].join('/')})
18
22
  end
19
23
  end
20
24
  end
data/lib/octopi/branch.rb CHANGED
@@ -1,18 +1,31 @@
1
1
  module Octopi
2
2
  class Branch < Base
3
+ attr_accessor :name, :sha
3
4
  include Resource
4
5
  set_resource_name "branch", "branches"
5
6
 
6
7
  resource_path "/repos/show/:id"
7
8
 
8
- def self.find(user, repo, api=ANONYMOUS_API)
9
- user = user.login if user.is_a? User
10
- repo = repo.name if repo.is_a? Repository
9
+ # Called when we ask for a resource.
10
+ # Arguments are passed in like [<name>, <sha>]
11
+ # TODO: Find out why args are doubly nested
12
+ def initialize(*args)
13
+ args = args.flatten!
14
+ self.name = args.first
15
+ self.sha = args.last
16
+ end
17
+
18
+ def to_s
19
+ name
20
+ end
21
+
22
+ def self.all(options={})
23
+ ensure_hash(options)
24
+ user, repo = gather_details(options)
11
25
  self.validate_args(user => :user, repo => :repo)
12
- api = ANONYMOUS_API if repo.is_a?(Repository) && !repo.private
13
- find_plural([user,repo,'branches'], :resource, api){
14
- |i| {:name => i.first, :hash => i.last }
15
- }
26
+ BranchSet.new(find_plural([user, repo, 'branches'], :resource)) do |i|
27
+ { :name => i.first, :hash => i.last }
28
+ end
16
29
  end
17
30
  end
18
31
  end
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), "branch")
2
+ class Octopi::BranchSet < Array
3
+ include Octopi
4
+ attr_accessor :user, :repository
5
+ # Takes a name, returns a branch if it exists
6
+ def find(name)
7
+ branch = detect { |b| b.name == name }
8
+ raise NotFound, Branch if branch.nil?
9
+ branch
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Octopi
2
+ class Comment < Base
3
+ attr_accessor :content, :author, :title, :updated, :link, :published, :id, :repository
4
+ include Resource
5
+ set_resource_name "tree"
6
+
7
+ resource_path "/tree/show/:id"
8
+
9
+ def self.find(options={})
10
+ ensure_hash(options)
11
+ user, repo, branch, sha = gather_details(options)
12
+ self.validate_args(sha => :sha, user => :user, repo => :repo)
13
+ super [user, repo, sha]
14
+ end
15
+
16
+ def commit
17
+ Commit.find(:user => repository.owner, :repo => repository, :sha => /commit\/(.*?)#/.match(link)[1])
18
+ end
19
+ end
20
+ end
data/lib/octopi/commit.rb CHANGED
@@ -4,51 +4,55 @@ module Octopi
4
4
  find_path "/commits/list/:query"
5
5
  resource_path "/commits/show/:id"
6
6
 
7
- attr_accessor :repository
7
+ attr_accessor :repository, :message, :parents, :author, :url, :id, :committed_date, :authored_date, :tree, :committer, :added, :removed, :modified
8
8
 
9
- # Finds all commits for a given Repository's branch
9
+
10
+ # Finds all commits for the given options:
10
11
  #
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.
12
+ # :repo or :repository or :name - A repository object or the name of a repository
13
+ # :user - A user object or the login of a user
14
+ # :branch - A branch object or the name of a branch. Defaults to master.
14
15
  #
15
- # If no branch is given, "master" is assumed.
16
- #
17
16
  # Sample usage:
18
17
  #
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
18
+ # >> find_all(:user => "fcoury", :repo => "octopi")
19
+ # => <Latest 30 commits for master branch>
20
+ #
21
+ # => find_all(:user => "fcoury", :repo => "octopi", :branch => "lazy") # branch is set to lazy.
22
+ # => <Latest 30 commits for lazy branch>
22
23
  #
23
- def self.find_all(*args)
24
- api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
25
- repo = args.first
26
- user ||= repo.owner if repo.is_a? Repository
27
- user, repo_name, opts = extract_user_repository(*args)
28
- self.validate_args(user => :user, repo_name => :repo)
29
- branch = opts[:branch] || "master"
30
- api = ANONYMOUS_API if repo.is_a?(Repository) && !repo.private
31
- commits = super user, repo_name, branch, api
32
- commits.each { |c| c.repository = repo } if repo.is_a? Repository
33
- commits
34
- end
35
-
36
- # TODO: Make find use hashes like find_all
37
- def self.find(*args)
38
- if args.last.is_a?(Commit)
39
- commit = args.pop
40
- super "#{commit.repo_identifier}"
24
+ def self.find_all(options={})
25
+ ensure_hash(options)
26
+ user, repo, branch = gather_details(options)
27
+ commits = if options[:path]
28
+ super user, repo.name, branch, options[:path]
41
29
  else
42
- user, name, sha = *args
43
- user = user.login if user.is_a? User
44
- name = repo.name if name.is_a? Repository
45
- self.validate_args(user => :user, name => :repo, sha => :sha)
46
- super [user, name, sha]
30
+ super user, repo.name, branch
47
31
  end
32
+ # Repository is not passed in from the data, set it manually.
33
+ commits.each { |c| c.repository = repo }
34
+ commits
48
35
  end
49
36
 
50
- def details
51
- self.class.find(self)
37
+ # Finds all commits for the given options:
38
+ #
39
+ # :repo or :repository or :name - A repository object or the name of a repository
40
+ # :user - A user object or the login of a user
41
+ # :branch - A branch object or the name of a branch. Defaults to master.
42
+ # :sha - The commit ID
43
+ #
44
+ # Sample usage:
45
+ #
46
+ # >> find(:user => "fcoury", :repo => "octopi", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae")
47
+ # => <Commit f6609209c3ac0badd004512d318bfaa508ea10ae for branch master>
48
+ #
49
+ # >> find(:user => "fcoury", :repo => "octopi", :branch => "lazy", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae") # branch is set to lazy.
50
+ # => <Commit f6609209c3ac0badd004512d318bfaa508ea10ae for branch lazy>
51
+ #
52
+ def self.find(options={})
53
+ ensure_hash(options)
54
+ user, repo, branch, sha = gather_details(options)
55
+ super [user, repo, sha]
52
56
  end
53
57
 
54
58
  def repo_identifier