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