devver-octopi 0.2.8

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