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/error.rb CHANGED
@@ -2,14 +2,17 @@ module Octopi
2
2
 
3
3
  class FormatError < StandardError
4
4
  def initialize(f)
5
- $stderr.puts "Got unexpected format (got #{f.first} for #{f.last})"
5
+ super("Got unexpected format (got #{f.first} for #{f.last})")
6
6
  end
7
- end
7
+ end
8
+
9
+ class AuthenticationRequired < StandardError
10
+ end
8
11
 
9
12
  class APIError < StandardError
10
- def initialize(m)
11
- $stderr.puts m
12
- end
13
+ end
14
+
15
+ class InvalidLogin < StandardError
13
16
  end
14
17
 
15
18
  class RetryableAPIError < RuntimeError
@@ -20,4 +23,13 @@ module Octopi
20
23
  super @message
21
24
  end
22
25
  end
26
+
27
+ class ArgumentMustBeHash < Exception; end
28
+
29
+
30
+ class NotFound < Exception
31
+ def initialize(klass)
32
+ super "The #{klass.to_s.split("::").last} you were looking for could not be found, or is private."
33
+ end
34
+ end
23
35
  end
@@ -1,15 +1,16 @@
1
1
  module Octopi
2
2
  class FileObject < Base
3
+ attr_accessor :name, :sha, :mode, :type
4
+
3
5
  include Resource
4
6
  set_resource_name "tree"
5
-
6
7
  resource_path "/tree/show/:id"
7
8
 
8
- def self.find(user, repo, sha)
9
- user = user.login if user.is_a? User
10
- repo = repo.name if repo.is_a? Repository
9
+ def self.find(options={})
10
+ ensure_hash(options)
11
+ user, repo, branch, sha = gather_details(options)
11
12
  self.validate_args(sha => :sha, user => :user, repo => :repo)
12
- super [user,repo,sha]
13
+ super [user, repo, sha]
13
14
  end
14
15
  end
15
16
  end
@@ -0,0 +1,28 @@
1
+ module Octopi
2
+ # Gist API is... lacking at the moment.
3
+ # This class serves only as a reminder to implement it later
4
+ class Gist < Base
5
+ include HTTParty
6
+ attr_accessor :description, :repo, :public, :created_at
7
+
8
+ include Resource
9
+ set_resource_name "tree"
10
+ resource_path ":id"
11
+
12
+ def self.base_uri
13
+ "http://gist.github.com/api/v1/yaml"
14
+ end
15
+
16
+ def self.find(id)
17
+ result = get("#{base_uri}/#{id}")
18
+ # This returns an array of Gists, rather than a single record.
19
+ new(result["gists"].first)
20
+ end
21
+
22
+ # def files
23
+ # gists_folder = File.join(ENV['HOME'], ".octopi", "gists")
24
+ # File.mkdir_p(gists_folder)
25
+ # `git clone git://`
26
+ # end
27
+ end
28
+ end
data/lib/octopi/issue.rb CHANGED
@@ -6,7 +6,15 @@ module Octopi
6
6
  find_path "/issues/list/:query"
7
7
  resource_path "/issues/show/:id"
8
8
 
9
- attr_accessor :repository
9
+
10
+ attr_accessor :repository, :user, :updated_at, :votes, :number, :title, :body, :closed_at, :labels, :state, :created_at
11
+
12
+ def self.search(options={})
13
+ ensure_hash(options)
14
+ options[:state] ||= "open"
15
+ user, repo = gather_details(options)
16
+ Api.api.get("/issues/search/#{user}/#{repo}/#{options[:state]}/#{options[:keyword]}")
17
+ end
10
18
 
11
19
  # Finds all issues for a given Repository
12
20
  #
@@ -22,72 +30,73 @@ module Octopi
22
30
  # find_all("octopi", :user => "fcoury") # user must be provided
23
31
  # find_all(:user => "fcoury", :repo => "octopi") # state defaults to open
24
32
  #
25
- def self.find_all(*args)
26
- repo = args.first
27
- user, repo_name, opts = extract_user_repository(*args)
28
- state = opts[:state] || "open"
29
- state.downcase! if state
30
- validate_args(user => :user, repo_name => :repo, state => :state)
33
+ def self.find_all(options={})
34
+ ensure_hash(options)
35
+ user, repo = gather_details(options)
36
+ state = (options[:state] || "open").downcase
37
+ validate_args(user => :user, repo.name => :repo, state => :state)
31
38
 
32
- issues = super user, repo_name, state
33
- issues.each { |i| i.repository = repo } if repo.is_a? Repository
39
+ issues = super user, repo.name, state
40
+ issues.each { |i| i.repository = repo }
34
41
  issues
35
42
  end
36
43
 
37
44
  # TODO: Make find use hashes like find_all
38
- def self.find(*args)
39
- if args.length < 2
40
- raise "Issue.find needs user, repository and issue number"
41
- end
42
-
43
- number = args.pop.to_i if args.last.respond_to?(:to_i)
44
- number = args.pop if args.last.is_a?(Integer)
45
+ def self.find(options={})
46
+ ensure_hash(options)
47
+ # Do not cache issues, as they may change via other means.
48
+ @cache = false
49
+ user, repo = gather_details(options)
45
50
 
46
- raise "Issue.find needs issue number as the last argument" unless number
47
-
48
- if args.length > 1
49
- user, repo = *args
50
- else
51
- repo = args.pop
52
- raise "Issue.find needs at least a Repository object and issue number" unless repo.is_a? Repository
53
- user, repo = repo.owner, repo.name
54
- end
55
-
56
- user, repo = extract_names(user, repo)
57
51
  validate_args(user => :user, repo => :repo)
58
- super user, repo, number
52
+ issue = super user, repo, options[:number]
53
+ issue.repository = repo
54
+ issue
59
55
  end
60
56
 
61
- def self.open(user, repo, params, api = ANONYMOUS_API)
62
- user, repo_name = extract_names(user, repo)
63
- data = api.post("/issues/open/#{user}/#{repo_name}", params)
64
- issue = new(api, data['issue'])
65
- issue.repository = repo if repo.is_a? Repository
57
+ def self.open(options={})
58
+ ensure_hash(options)
59
+ user, repo = gather_details(options)
60
+ data = Api.api.post("/issues/open/#{user}/#{repo.name}", options[:params])
61
+ issue = new(data['issue'])
62
+ issue.repository = repo
66
63
  issue
67
64
  end
68
65
 
69
- def reopen(*args)
70
- data = @api.post(command_path("reopen"))
66
+ # Re-opens an issue.
67
+ def reopen!
68
+ data = Api.api.post(command_path("reopen"))
69
+ self.state = 'open'
70
+ self
71
71
  end
72
72
 
73
- def close(*args)
74
- data = @api.post(command_path("close"))
73
+ def close!
74
+ data = Api.api.post(command_path("close"))
75
+ self.state = 'closed'
76
+ self
75
77
  end
76
78
 
77
79
  def save
78
- data = @api.post(command_path("edit"), { :title => self.title, :body => self.body })
80
+ data = Api.api.post(command_path("edit"), { :title => title, :body => body })
81
+ self
79
82
  end
80
83
 
81
84
  %w(add remove).each do |oper|
82
85
  define_method("#{oper}_label") do |*labels|
83
86
  labels.each do |label|
84
- @api.post("#{prefix("label/#{oper}")}/#{label}/#{number}")
87
+ Api.api.post("#{prefix("label/#{oper}")}/#{label}/#{number}", { :cache => false })
88
+ if oper == "add"
89
+ self.labels << label
90
+ else
91
+ self.labels -= [label]
92
+ end
85
93
  end
86
94
  end
87
95
  end
88
96
 
89
97
  def comment(comment)
90
- @api.post(command_path("comment"), { :comment => comment })
98
+ data = Api.api.post(command_path("comment"), { :comment => comment })
99
+ IssueComment.new(data['comment'])
91
100
  end
92
101
 
93
102
  private
@@ -0,0 +1,7 @@
1
+ module Octopi
2
+ class IssueComment < Base
3
+ include Resource
4
+ attr_accessor :comment, :status
5
+
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ require File.join(File.dirname(__FILE__), "issue")
2
+ class Octopi::IssueSet < Array
3
+ include Octopi
4
+ attr_accessor :user, :repository
5
+
6
+ def initialize(array)
7
+ self.user = array.first.user
8
+ self.repository = array.first.repository
9
+ super(array)
10
+ end
11
+
12
+ def find(number)
13
+ issue = detect { |issue| issue.number == number }
14
+ raise NotFound, Issue if issue.nil?
15
+ issue
16
+ end
17
+
18
+ def search(options={})
19
+ Issue.search(options.merge(:user => user, :repo => repository))
20
+ end
21
+ end
data/lib/octopi/key.rb CHANGED
@@ -2,17 +2,24 @@ module Octopi
2
2
  class Key < Base
3
3
  include Resource
4
4
 
5
+ attr_accessor :title, :id, :key
6
+ find_path "/user/keys"
7
+
5
8
  attr_reader :user
6
-
7
- def initialize(api, data, user = nil)
8
- super api, data
9
- @user = user
9
+
10
+ def self.find_all
11
+ Api.api.get("user/keys")
12
+ end
13
+
14
+ def self.add(options={})
15
+ ensure_hash(options)
16
+ Api.api.post("/user/key/add", { :title => options[:title], :key => options[:key], :cache => false })
17
+
10
18
  end
11
19
 
12
- def remove!
13
- result = @api.post "/user/key/remove", :id => id
20
+ def remove
21
+ result = Api.api.post "/user/key/remove", { :id => id, :cache => false }
14
22
  keys = result["public_keys"].select { |k| k["title"] == title }
15
- keys.empty?
16
23
  end
17
24
  end
18
25
  end
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), "key")
2
+ class KeySet < Array
3
+ include Octopi
4
+ def find(title)
5
+ key = detect { |key| key.title == title }
6
+ raise NotFound, Key if key.nil?
7
+ key
8
+ end
9
+
10
+ def add(options={})
11
+ ensure_hash(options)
12
+ Key.add(options)
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Octopi
2
+ class Plan < Base
3
+ attr_accessor :name, :collaborators, :space, :private_repos
4
+ end
5
+ end
@@ -1,6 +1,11 @@
1
1
  module Octopi
2
2
  class Repository < Base
3
3
  include Resource
4
+ attr_accessor :description, :url, :forks, :name, :homepage, :watchers,
5
+ :owner, :private, :fork, :open_issues, :pledgie, :size,
6
+ # And now for the stuff returned by search results
7
+ :actions, :score, :language, :followers, :type, :username,
8
+ :id, :pushed, :created
4
9
  set_resource_name "repository", "repositories"
5
10
 
6
11
  create_path "/repos/create"
@@ -10,6 +15,10 @@ module Octopi
10
15
 
11
16
  attr_accessor :private
12
17
 
18
+ def owner=(owner)
19
+ @owner = User.find(owner)
20
+ end
21
+
13
22
  # Returns all branches for the Repository
14
23
  #
15
24
  # Example:
@@ -17,7 +26,7 @@ module Octopi
17
26
  # repo.branches.each { |r| puts r.name }
18
27
  #
19
28
  def branches
20
- Branch.find(self.owner, self.name,api)
29
+ Branch.all(:user => self.owner, :repo => self)
21
30
  end
22
31
 
23
32
  # Returns all tags for the Repository
@@ -27,36 +36,50 @@ module Octopi
27
36
  # repo.tags.each { |t| puts t.name }
28
37
  #
29
38
  def tags
30
- Tag.find(self.owner, self.name)
31
- end
39
+ Tag.all(:user => self.owner, :repo => self)
40
+ end
32
41
 
33
- def clone_url
34
- if private? || api.login == self.owner
35
- "git@github.com:#{self.owner}/#{self.name}.git"
36
- else
37
- "git://github.com/#{self.owner}/#{self.name}.git"
42
+
43
+ # Returns all the comments for a Repository
44
+ def comments
45
+ # We have to specify xmlns as a prefix as the document is namespaced.
46
+ # Be wary!
47
+ path = "http#{'s' if private}://github.com/#{owner}/#{name}/comments.atom"
48
+ xml = Nokogiri::XML(Net::HTTP.get(URI.parse(path)))
49
+ entries = xml.xpath("//xmlns:entry")
50
+ comments = []
51
+ for entry in entries
52
+ content = entry.xpath("xmlns:content").text.gsub("&lt;", "<").gsub("&gt;", ">")
53
+ comments << Comment.new(
54
+ :id => entry.xpath("xmlns:id"),
55
+ :published => Time.parse(entry.xpath("xmlns:published").text),
56
+ :updated => Time.parse(entry.xpath("xmlns:updated").text),
57
+ :link => entry.xpath("xmlns:link/@href").text,
58
+ :title => entry.xpath("xmlns:title").text,
59
+ :content => content,
60
+ :author => entry.xpath("xmlns:author/xmlns:name").text,
61
+ :repository => self
62
+ )
38
63
  end
64
+ comments
39
65
  end
40
-
41
- def self.find_by_user(user, api = ANONYMOUS_API)
42
- user = user.login if user.is_a? User
43
- self.validate_args(user => :user)
44
- find_plural(user, :resource, api)
66
+
67
+ def clone_url
68
+ url = private || Api.api.login == self.owner.login ? "git@github.com:" : "git://github.com/"
69
+ url += "#{self.owner}/#{self.name}.git"
45
70
  end
46
-
47
- def self.find(*args)
48
- api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
49
- repo = args.pop
50
- user = args.pop
51
-
52
- user = user.login if user.is_a? User
53
- if repo.is_a? Repository
54
- repo = repo.name
55
- user ||= repo.owner
56
- end
71
+
72
+ def self.find(options={})
73
+ ensure_hash(options)
74
+ # Lots of people call the same thing differently.
75
+ # Can't call gather_details here because this method is used by it internally.
76
+ repo = options[:repo] || options[:repository] || options[:name]
77
+ user = options[:user].to_s
78
+
79
+ return find_plural(user, :resource) if repo.nil?
57
80
 
58
81
  self.validate_args(user => :user, repo => :repo)
59
- super user, repo, api
82
+ super user, repo
60
83
  end
61
84
 
62
85
  def self.find_all(*args)
@@ -65,21 +88,16 @@ module Octopi
65
88
  super args.join(" ").gsub(/ /,'+')
66
89
  end
67
90
 
68
- def self.open_issue(args)
69
- Issue.open(args[:user], args[:repo], args)
70
- end
71
-
72
- def open_issue(args)
73
- Issue.open(self.owner, self, args, @api)
91
+ class << self
92
+ alias_method :search, :find_all
74
93
  end
75
94
 
76
95
  def commits(branch = "master")
77
- api = self.api || ANONYMOUS_API
78
- Commit.find_all(self, {:branch => branch}, api)
96
+ Commit.find_all(:user => self.owner, :repo => self, :branch => branch)
79
97
  end
80
98
 
81
99
  def issues(state = "open")
82
- Issue.find_all(self, :state => state)
100
+ IssueSet.new(Octopi::Issue.find_all(:user => owner, :repository => self))
83
101
  end
84
102
 
85
103
  def all_issues
@@ -87,24 +105,27 @@ module Octopi
87
105
  end
88
106
 
89
107
  def issue(number)
90
- Issue.find(self, number)
108
+ Issue.find(:user => self.owner, :repo => self, :number => number)
91
109
  end
92
110
 
93
111
  def collaborators
94
- property('collaborators', [self.owner,self.name].join('/')).values
112
+ property('collaborators', [self.owner, self.name].join('/')).values.map { |v| User.find(v) }
95
113
  end
96
114
 
97
- def self.create(owner, name, opts = {})
98
- api = owner.is_a?(User) ? owner.api : ANONYMOUS_API
99
- raise APIError, "To create a repository you must be authenticated." if api.read_only?
100
- self.validate_args(name => :repo)
101
- api.post(path_for(:create), opts.merge(:name => name))
102
- self.find(owner, name, api)
115
+ def self.create(options={})
116
+ raise AuthenticationRequired, "To create a repository you must be authenticated." if Api.api.read_only?
117
+ self.validate_args(options[:name] => :repo)
118
+ new(Api.api.post(path_for(:create), options)["repository"])
119
+ end
120
+
121
+ def delete!
122
+ raise APIError, "You must be authenticated as the owner of this repository to delete it" if Api.me.login != owner.login
123
+ token = Api.api.post(self.class.path_for(:delete), :id => self.name)['delete_token']
124
+ Api.api.post(self.class.path_for(:delete), :id => self.name, :delete_token => token) unless token.nil?
103
125
  end
104
126
 
105
- def delete
106
- token = @api.post(self.class.path_for(:delete), :id => self.name)['delete_token']
107
- @api.post(self.class.path_for(:delete), :id => self.name, :delete_token => token) unless token.nil?
127
+ def to_s
128
+ name
108
129
  end
109
130
 
110
131
  end