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