devver-octopi 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/.yardoc +0 -0
  3. data/CHANGELOG.md +9 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +144 -0
  6. data/Rakefile +100 -0
  7. data/VERSION.yml +5 -0
  8. data/contrib/backup.rb +100 -0
  9. data/examples/authenticated.rb +20 -0
  10. data/examples/issues.rb +18 -0
  11. data/examples/overall.rb +50 -0
  12. data/lib/ext/hash_ext.rb +5 -0
  13. data/lib/ext/string_ext.rb +5 -0
  14. data/lib/octopi.rb +136 -0
  15. data/lib/octopi/api.rb +213 -0
  16. data/lib/octopi/base.rb +115 -0
  17. data/lib/octopi/blob.rb +25 -0
  18. data/lib/octopi/branch.rb +31 -0
  19. data/lib/octopi/branch_set.rb +11 -0
  20. data/lib/octopi/comment.rb +20 -0
  21. data/lib/octopi/commit.rb +69 -0
  22. data/lib/octopi/error.rb +35 -0
  23. data/lib/octopi/file_object.rb +16 -0
  24. data/lib/octopi/gist.rb +28 -0
  25. data/lib/octopi/issue.rb +111 -0
  26. data/lib/octopi/issue_comment.rb +7 -0
  27. data/lib/octopi/issue_set.rb +21 -0
  28. data/lib/octopi/key.rb +25 -0
  29. data/lib/octopi/key_set.rb +14 -0
  30. data/lib/octopi/plan.rb +5 -0
  31. data/lib/octopi/repository.rb +132 -0
  32. data/lib/octopi/repository_set.rb +9 -0
  33. data/lib/octopi/resource.rb +70 -0
  34. data/lib/octopi/self.rb +33 -0
  35. data/lib/octopi/tag.rb +23 -0
  36. data/lib/octopi/user.rb +123 -0
  37. data/octopi.gemspec +99 -0
  38. data/test/api_test.rb +58 -0
  39. data/test/authenticated_test.rb +39 -0
  40. data/test/blob_test.rb +23 -0
  41. data/test/branch_test.rb +20 -0
  42. data/test/commit_test.rb +82 -0
  43. data/test/file_object_test.rb +39 -0
  44. data/test/gist_test.rb +16 -0
  45. data/test/issue_comment.rb +19 -0
  46. data/test/issue_set_test.rb +33 -0
  47. data/test/issue_test.rb +120 -0
  48. data/test/key_set_test.rb +29 -0
  49. data/test/key_test.rb +35 -0
  50. data/test/repository_set_test.rb +23 -0
  51. data/test/repository_test.rb +151 -0
  52. data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
  53. data/test/tag_test.rb +20 -0
  54. data/test/test_helper.rb +246 -0
  55. data/test/user_test.rb +92 -0
  56. metadata +153 -0
@@ -0,0 +1,25 @@
1
+ require File.join(File.dirname(__FILE__), "resource")
2
+ module Octopi
3
+ class Blob < Base
4
+ attr_accessor :text, :data, :name, :sha, :size, :mode, :mime_type
5
+ include Resource
6
+ set_resource_name "blob"
7
+
8
+ resource_path "/blob/show/:id"
9
+
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
+
18
+ if path
19
+ super [user, repo, sha, path]
20
+ else
21
+ Api.api.get_raw(path_for(:resource), {:id => [user, repo, sha].join('/')})
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ module Octopi
2
+ class Branch < Base
3
+ attr_accessor :name, :sha
4
+ include Resource
5
+ set_resource_name "branch", "branches"
6
+
7
+ resource_path "/repos/show/:id"
8
+
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)
25
+ self.validate_args(user => :user, repo => :repo)
26
+ BranchSet.new(find_plural([user, repo, 'branches'], :resource)) do |i|
27
+ { :name => i.first, :hash => i.last }
28
+ end
29
+ end
30
+ end
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
@@ -0,0 +1,69 @@
1
+ module Octopi
2
+ class Commit < Base
3
+ include Resource
4
+ find_path "/commits/list/:query"
5
+ resource_path "/commits/show/:id"
6
+
7
+ attr_accessor :repository, :message, :parents, :author, :url, :id, :committed_date, :authored_date, :tree, :committer, :added, :removed, :modified
8
+
9
+
10
+ # Finds all commits for the given options:
11
+ #
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.
15
+ #
16
+ # Sample usage:
17
+ #
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>
23
+ #
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]
29
+ else
30
+ super user, repo.name, branch
31
+ end
32
+ # Repository is not passed in from the data, set it manually.
33
+ commits.each { |c| c.repository = repo }
34
+ commits
35
+ end
36
+
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]
56
+ end
57
+
58
+ def repo_identifier
59
+ url_parts = url.split('/')
60
+ if @repository
61
+ parts = [@repository.owner, @repository.name, url_parts[6]]
62
+ else
63
+ parts = [url_parts[3], url_parts[4], url_parts[6]]
64
+ end
65
+
66
+ parts.join('/')
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ module Octopi
2
+
3
+ class FormatError < StandardError
4
+ def initialize(f)
5
+ super("Got unexpected format (got #{f.first} for #{f.last})")
6
+ end
7
+ end
8
+
9
+ class AuthenticationRequired < StandardError
10
+ end
11
+
12
+ class APIError < StandardError
13
+ end
14
+
15
+ class InvalidLogin < StandardError
16
+ end
17
+
18
+ class RetryableAPIError < RuntimeError
19
+ attr_reader :code
20
+ def initialize(code=nil)
21
+ @code = code.nil? ? '???' : code
22
+ @message = "GitHub returned status #{@code}. Retrying request."
23
+ super @message
24
+ end
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
35
+ end
@@ -0,0 +1,16 @@
1
+ module Octopi
2
+ class FileObject < Base
3
+ attr_accessor :name, :sha, :mode, :type
4
+
5
+ include Resource
6
+ set_resource_name "tree"
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
+ end
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
@@ -0,0 +1,111 @@
1
+ module Octopi
2
+ class Issue < Base
3
+ include Resource
4
+ STATES = %w{open closed}
5
+
6
+ find_path "/issues/list/:query"
7
+ resource_path "/issues/show/:id"
8
+
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
18
+
19
+ # Finds all issues for a given Repository
20
+ #
21
+ # You can provide the user and repo parameters as
22
+ # String or as User and Repository objects. When repo
23
+ # is provided as a Repository object, user is superfluous.
24
+ #
25
+ # If no state is given, "open" is assumed.
26
+ #
27
+ # Sample usage:
28
+ #
29
+ # find_all(repo, :state => "closed") # repo must be an object
30
+ # find_all("octopi", :user => "fcoury") # user must be provided
31
+ # find_all(:user => "fcoury", :repo => "octopi") # state defaults to open
32
+ #
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)
38
+
39
+ issues = super user, repo.name, state
40
+ issues.each { |i| i.repository = repo }
41
+ issues
42
+ end
43
+
44
+ # TODO: Make find use hashes like find_all
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)
50
+
51
+ validate_args(user => :user, repo => :repo)
52
+ issue = super user, repo, options[:number]
53
+ issue.repository = repo
54
+ issue
55
+ end
56
+
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
63
+ issue
64
+ end
65
+
66
+ # Re-opens an issue.
67
+ def reopen!
68
+ data = Api.api.post(command_path("reopen"))
69
+ self.state = 'open'
70
+ self
71
+ end
72
+
73
+ def close!
74
+ data = Api.api.post(command_path("close"))
75
+ self.state = 'closed'
76
+ self
77
+ end
78
+
79
+ def save
80
+ data = Api.api.post(command_path("edit"), { :title => title, :body => body })
81
+ self
82
+ end
83
+
84
+ %w(add remove).each do |oper|
85
+ define_method("#{oper}_label") do |*labels|
86
+ labels.each do |label|
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
93
+ end
94
+ end
95
+ end
96
+
97
+ def comment(comment)
98
+ data = Api.api.post(command_path("comment"), { :comment => comment })
99
+ IssueComment.new(data['comment'])
100
+ end
101
+
102
+ private
103
+ def prefix(command)
104
+ "/issues/#{command}/#{repository.owner}/#{repository.name}"
105
+ end
106
+
107
+ def command_path(command)
108
+ "#{prefix(command)}/#{number}"
109
+ end
110
+ end
111
+ end
@@ -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 ADDED
@@ -0,0 +1,25 @@
1
+ module Octopi
2
+ class Key < Base
3
+ include Resource
4
+
5
+ attr_accessor :title, :id, :key
6
+ find_path "/user/keys"
7
+
8
+ attr_reader :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
+
18
+ end
19
+
20
+ def remove
21
+ result = Api.api.post "/user/key/remove", { :id => id, :cache => false }
22
+ keys = result["public_keys"].select { |k| k["title"] == title }
23
+ end
24
+ end
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
@@ -0,0 +1,132 @@
1
+ module Octopi
2
+ class Repository < Base
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
9
+ set_resource_name "repository", "repositories"
10
+
11
+ create_path "/repos/create"
12
+ find_path "/repos/search/:query"
13
+ resource_path "/repos/show/:id"
14
+ delete_path "/repos/delete/:id"
15
+
16
+ attr_accessor :private
17
+
18
+ def owner=(owner)
19
+ @owner = User.find(owner)
20
+ end
21
+
22
+ # Returns all branches for the Repository
23
+ #
24
+ # Example:
25
+ # repo = Repository.find("fcoury", "octopi")
26
+ # repo.branches.each { |r| puts r.name }
27
+ #
28
+ def branches
29
+ Branch.all(:user => self.owner, :repo => self)
30
+ end
31
+
32
+ # Returns all tags for the Repository
33
+ #
34
+ # Example:
35
+ # repo = Repository.find("fcoury", "octopi")
36
+ # repo.tags.each { |t| puts t.name }
37
+ #
38
+ def tags
39
+ Tag.all(:user => self.owner, :repo => self)
40
+ end
41
+
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
+ )
63
+ end
64
+ comments
65
+ end
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"
70
+ 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?
80
+
81
+ self.validate_args(user => :user, repo => :repo)
82
+ super user, repo
83
+ end
84
+
85
+ def self.find_all(*args)
86
+ # FIXME: This should be URI escaped, but have to check how the API
87
+ # handles escaped characters first.
88
+ super args.join(" ").gsub(/ /,'+')
89
+ end
90
+
91
+ class << self
92
+ alias_method :search, :find_all
93
+ end
94
+
95
+ def commits(branch = "master")
96
+ Commit.find_all(:user => self.owner, :repo => self, :branch => branch)
97
+ end
98
+
99
+ def issues(state = "open")
100
+ IssueSet.new(Octopi::Issue.find_all(:user => owner, :repository => self))
101
+ end
102
+
103
+ def all_issues
104
+ Issue::STATES.map{|state| self.issues(state)}.flatten
105
+ end
106
+
107
+ def issue(number)
108
+ Issue.find(:user => self.owner, :repo => self, :number => number)
109
+ end
110
+
111
+ def collaborators
112
+ property('collaborators', [self.owner, self.name].join('/')).values.map { |v| User.find(v) }
113
+ end
114
+
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?
125
+ end
126
+
127
+ def to_s
128
+ name
129
+ end
130
+
131
+ end
132
+ end