dcuddeback-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 (58) 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.markdown +144 -0
  6. data/Rakefile +94 -0
  7. data/VERSION.yml +4 -0
  8. data/contrib/backup.rb +100 -0
  9. data/dcuddeback-octopi.gemspec +108 -0
  10. data/examples/authenticated.rb +20 -0
  11. data/examples/issues.rb +18 -0
  12. data/examples/overall.rb +50 -0
  13. data/lib/ext/string_ext.rb +5 -0
  14. data/lib/octopi/api.rb +213 -0
  15. data/lib/octopi/base.rb +115 -0
  16. data/lib/octopi/blob.rb +25 -0
  17. data/lib/octopi/branch.rb +31 -0
  18. data/lib/octopi/branch_set.rb +11 -0
  19. data/lib/octopi/comment.rb +20 -0
  20. data/lib/octopi/commit.rb +69 -0
  21. data/lib/octopi/deploy_key.rb +27 -0
  22. data/lib/octopi/deploy_key_set.rb +18 -0
  23. data/lib/octopi/error.rb +35 -0
  24. data/lib/octopi/file_object.rb +16 -0
  25. data/lib/octopi/gist.rb +28 -0
  26. data/lib/octopi/issue.rb +111 -0
  27. data/lib/octopi/issue_comment.rb +7 -0
  28. data/lib/octopi/issue_set.rb +21 -0
  29. data/lib/octopi/key.rb +25 -0
  30. data/lib/octopi/key_set.rb +14 -0
  31. data/lib/octopi/plan.rb +5 -0
  32. data/lib/octopi/repository.rb +136 -0
  33. data/lib/octopi/repository_set.rb +9 -0
  34. data/lib/octopi/resource.rb +70 -0
  35. data/lib/octopi/self.rb +33 -0
  36. data/lib/octopi/tag.rb +23 -0
  37. data/lib/octopi/user.rb +131 -0
  38. data/lib/octopi.rb +135 -0
  39. data/test/api_test.rb +58 -0
  40. data/test/authenticated_test.rb +39 -0
  41. data/test/base_test.rb +20 -0
  42. data/test/blob_test.rb +23 -0
  43. data/test/branch_test.rb +20 -0
  44. data/test/commit_test.rb +82 -0
  45. data/test/file_object_test.rb +39 -0
  46. data/test/gist_test.rb +16 -0
  47. data/test/issue_comment.rb +19 -0
  48. data/test/issue_set_test.rb +33 -0
  49. data/test/issue_test.rb +120 -0
  50. data/test/key_set_test.rb +29 -0
  51. data/test/key_test.rb +35 -0
  52. data/test/repository_set_test.rb +23 -0
  53. data/test/repository_test.rb +151 -0
  54. data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
  55. data/test/tag_test.rb +20 -0
  56. data/test/test_helper.rb +246 -0
  57. data/test/user_test.rb +92 -0
  58. metadata +151 -0
@@ -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,136 @@
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.join) }
113
+ end
114
+
115
+ def deploy_keys
116
+ DeployKey.all(:user => self.owner, :repo => self)
117
+ end
118
+
119
+ def self.create(options={})
120
+ raise AuthenticationRequired, "To create a repository you must be authenticated." if Api.api.read_only?
121
+ self.validate_args(options[:name] => :repo)
122
+ new(Api.api.post(path_for(:create), options.merge( :cache => false ))["repository"])
123
+ end
124
+
125
+ def delete!
126
+ raise APIError, "You must be authenticated as the owner of this repository to delete it" if Api.me.login != owner.login
127
+ token = Api.api.post(self.class.path_for(:delete), :id => self.name)['delete_token']
128
+ Api.api.post(self.class.path_for(:delete), :id => self.name, :delete_token => token) unless token.nil?
129
+ end
130
+
131
+ def to_s
132
+ name
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), "repository")
2
+ class Octopi::RepositorySet < Array
3
+ include Octopi
4
+ attr_accessor :user
5
+
6
+ def find(name)
7
+ Octopi::Repository.find(:user => self.user, :repository => name)
8
+ end
9
+ end
@@ -0,0 +1,70 @@
1
+ module Octopi
2
+ module Resource
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.set_resource_name(base.name)
6
+ (@@resources||={})[base.resource_name(:singular)] = base
7
+ (@@resources||={})[base.resource_name(:plural)] = base
8
+ end
9
+
10
+ def self.for(name)
11
+ @@resources[name]
12
+ end
13
+
14
+ module ClassMethods
15
+ def set_resource_name(singular, plural = "#{singular}s")
16
+ @resource_name = {:singular => declassify(singular), :plural => declassify(plural)}
17
+ end
18
+
19
+ def resource_name(key)
20
+ @resource_name[key]
21
+ end
22
+
23
+ def create_path(path)
24
+ (@path_spec||={})[:create] = path
25
+ end
26
+
27
+ def find_path(path)
28
+ (@path_spec||={})[:find] = path
29
+ end
30
+
31
+ def resource_path(path)
32
+ (@path_spec||={})[:resource] = path
33
+ end
34
+
35
+ def delete_path(path)
36
+ (@path_spec||={})[:delete] = path
37
+ end
38
+
39
+ def find(*args)
40
+ args = args.join('/') if args.is_a? Array
41
+ result = Api.api.find(path_for(:resource), @resource_name[:singular], args, self, @cache)
42
+ key = result.keys.first
43
+
44
+ if result[key].is_a? Array
45
+ result[key].map { |r| new(r) }
46
+ else
47
+ Resource.for(key).new(result[key])
48
+ end
49
+ end
50
+
51
+ def find_all(*s)
52
+ find_plural(s, :find)
53
+ end
54
+
55
+ def find_plural(s, path)
56
+ s = s.join('/') if s.is_a? Array
57
+ resources = Api.api.find_all(path_for(path), @resource_name[:plural], s, self)
58
+ resources.map { |item| self.new(item) }
59
+ end
60
+
61
+ def declassify(s)
62
+ (s.split('::').last || '').downcase if s
63
+ end
64
+
65
+ def path_for(type)
66
+ @path_spec[type]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,33 @@
1
+ module Octopi
2
+ module Self
3
+ # Returns a list of Key objects containing all SSH Public Keys this user
4
+ # currently has. Requires authentication.
5
+ def keys
6
+ raise AuthenticationRequired, "To view keys, you must be authenticated" if Api.api.read_only?
7
+ result = Api.api.get("/user/keys", { :cache => false })
8
+ return unless result and result["public_keys"]
9
+ KeySet.new(result["public_keys"].inject([]) { |result, element| result << Key.new(element) })
10
+ end
11
+
12
+ # Returns a list of Email objects containing the email addresses associated with this user.
13
+ # Requires authentication.
14
+ def emails
15
+ raise AuthenticationRequired, "To view emails, you must be authenticated" if Api.api.read_only?
16
+ get("/user/emails")['emails']
17
+ end
18
+
19
+ # Start following a user.
20
+ # Can only be called if you are authenticated.
21
+ def follow!(login)
22
+ raise AuthenticationRequired, "To begin following someone, you must be authenticated" if Api.api.read_only?
23
+ Api.api.post("/user/follow/#{login}")
24
+ end
25
+
26
+ # Stop following a user.
27
+ # Can only be called if you are authenticated.
28
+ def unfollow!(login)
29
+ raise AuthenticationRequired, "To stop following someone, you must be authenticated" if Api.api.read_only?
30
+ Api.api.post("/user/unfollow/#{login}")
31
+ end
32
+ end
33
+ end
data/lib/octopi/tag.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Octopi
2
+ class Tag < Base
3
+ include Resource
4
+
5
+ attr_accessor :name, :sha
6
+ set_resource_name "tag"
7
+
8
+ resource_path "/repos/show/:id"
9
+
10
+ def initialize(*args)
11
+ args = args.flatten!
12
+ self.name = args.first
13
+ self.sha = args.last
14
+ end
15
+
16
+ def self.all(options={})
17
+ ensure_hash(options)
18
+ user, repo = gather_details(options)
19
+ self.validate_args(user => :user, repo => :repo)
20
+ find_plural([user, repo, 'tags'], :resource) { |i| Tag.new(i) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,131 @@
1
+ module Octopi
2
+ class User < Base
3
+ include Resource
4
+ attr_accessor :company, :name, :following_count, :gravatar_id,
5
+ :blog, :public_repo_count, :public_gist_count,
6
+ :id, :login, :followers_count, :created_at,
7
+ :email, :location, :disk_usage, :private_repo_count,
8
+ :private_gist_count, :collaborators, :plan,
9
+ :owned_private_repo_count, :total_private_repo_count,
10
+
11
+ # These come from search results, which doesn't
12
+ # contain the above information.
13
+ :actions, :score, :language, :followers, :following,
14
+ :fullname, :type, :username, :repos, :pushed, :created
15
+
16
+ def plan=(attributes={})
17
+ @plan = Plan.new(attributes)
18
+ end
19
+
20
+ find_path "/user/search/:query"
21
+ resource_path "/user/show/:id"
22
+
23
+ # Finds a single user identified by the given username
24
+ #
25
+ # Example:
26
+ #
27
+ # user = User.find("fcoury")
28
+ # puts user.login # should return 'fcoury'
29
+ def self.find(username)
30
+ self.validate_args(username => :user)
31
+ super username
32
+ end
33
+
34
+ # Finds all users whose username matches a given string
35
+ #
36
+ # Example:
37
+ #
38
+ # User.find_all("oe") # Matches joe, moe and monroe
39
+ #
40
+ def self.find_all(username)
41
+ self.validate_args(username => :user)
42
+ super username
43
+ end
44
+
45
+ class << self
46
+ alias_method :search, :find_all
47
+ end
48
+
49
+ # Returns a collection of Repository objects, containing
50
+ # all repositories of the user.
51
+ #
52
+ # If user is the current authenticated user, some
53
+ # additional information will be provided for the
54
+ # Repositories.
55
+ def repositories
56
+ rs = RepositorySet.new(Repository.find(:user => self.login))
57
+ rs.user = self
58
+ rs
59
+ end
60
+
61
+ # Searches for user Repository identified by name
62
+ def repository(options={})
63
+ options = { :name => options } if options.is_a?(String)
64
+ self.class.ensure_hash(options)
65
+ Repository.find({ :user => login }.merge!(options))
66
+ end
67
+
68
+ def create_repository(name, options = {})
69
+ self.class.validate_args(name => :repo)
70
+ Repository.create(self, name, options)
71
+ end
72
+
73
+ def watching
74
+ repositories = []
75
+ Api.api.get("/repos/watched/#{login}")["repositories"].each do |repo|
76
+ repositories << Repository.new(repo)
77
+ end
78
+ repositories
79
+ end
80
+
81
+
82
+ # Gets a list of followers.
83
+ # Returns an array of logins.
84
+ def followers
85
+ user_property("followers")
86
+ end
87
+
88
+ # Gets a list of followers.
89
+ # Returns an array of user objects.
90
+ # If user has a large number of followers you may be rate limited by the API.
91
+ def followers!
92
+ user_property("followers", true)
93
+ end
94
+
95
+ # Gets a list of people this user is following.
96
+ # Returns an array of logins.
97
+ def following
98
+ user_property("following")
99
+ end
100
+
101
+ # Gets a list of people this user is following.
102
+ # Returns an array of user objectrs.
103
+ # If user has a large number of people whom they follow, you may be rate limited by the API.
104
+ def following!
105
+ user_property("following", true)
106
+ end
107
+
108
+
109
+ # If a user object is passed into a method, we can use this.
110
+ # It'll also work if we pass in just the login.
111
+ def to_s
112
+ login
113
+ end
114
+
115
+ private
116
+
117
+ # Helper method for "deep" finds.
118
+ # Determines whether to return an array of logins (light) or user objects (heavy).
119
+ def user_property(property, deep=false)
120
+ users = []
121
+ property(property, login).each_pair do |k,v|
122
+ return v unless deep
123
+
124
+ v.each { |u| users << User.find(u) }
125
+ end
126
+
127
+ users
128
+ end
129
+
130
+ end
131
+ end