ddollar-octopi 0.0.13
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.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +169 -0
- data/Rakefile +83 -0
- data/VERSION.yml +4 -0
- data/contrib/backup.rb +100 -0
- data/examples/authenticated.rb +20 -0
- data/examples/github.yml.example +14 -0
- data/examples/issues.rb +18 -0
- data/examples/overall.rb +50 -0
- data/lib/octopi.rb +236 -0
- data/lib/octopi/base.rb +111 -0
- data/lib/octopi/blob.rb +21 -0
- data/lib/octopi/branch.rb +18 -0
- data/lib/octopi/commit.rb +65 -0
- data/lib/octopi/error.rb +23 -0
- data/lib/octopi/file_object.rb +15 -0
- data/lib/octopi/issue.rb +102 -0
- data/lib/octopi/key.rb +18 -0
- data/lib/octopi/repository.rb +111 -0
- data/lib/octopi/resource.rb +75 -0
- data/lib/octopi/tag.rb +17 -0
- data/lib/octopi/user.rb +99 -0
- data/octopi.gemspec +66 -0
- data/test/octopi_test.rb +46 -0
- data/test/test_helper.rb +10 -0
- metadata +83 -0
data/lib/octopi/error.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Octopi
|
2
|
+
|
3
|
+
class FormatError < StandardError
|
4
|
+
def initialize(f)
|
5
|
+
$stderr.puts "Got unexpected format (got #{f.first} for #{f.last})"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class APIError < StandardError
|
10
|
+
def initialize(m)
|
11
|
+
$stderr.puts m
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RetryableAPIError < RuntimeError
|
16
|
+
attr_reader :code
|
17
|
+
def initialize(code=nil)
|
18
|
+
@code = code.nil? ? '???' : code
|
19
|
+
@message = "GitHub returned status #{@code}. Retrying request."
|
20
|
+
super @message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Octopi
|
2
|
+
class FileObject < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "tree"
|
5
|
+
|
6
|
+
resource_path "/tree/show/:id"
|
7
|
+
|
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
|
11
|
+
self.validate_args(sha => :sha, user => :user, repo => :repo)
|
12
|
+
super [user,repo,sha]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/octopi/issue.rb
ADDED
@@ -0,0 +1,102 @@
|
|
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
|
+
attr_accessor :repository
|
10
|
+
|
11
|
+
# Finds all issues for a given Repository
|
12
|
+
#
|
13
|
+
# You can provide the user and repo parameters as
|
14
|
+
# String or as User and Repository objects. When repo
|
15
|
+
# is provided as a Repository object, user is superfluous.
|
16
|
+
#
|
17
|
+
# If no state is given, "open" is assumed.
|
18
|
+
#
|
19
|
+
# Sample usage:
|
20
|
+
#
|
21
|
+
# find_all(repo, :state => "closed") # repo must be an object
|
22
|
+
# find_all("octopi", :user => "fcoury") # user must be provided
|
23
|
+
# find_all(:user => "fcoury", :repo => "octopi") # state defaults to open
|
24
|
+
#
|
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)
|
31
|
+
|
32
|
+
issues = super user, repo_name, state
|
33
|
+
issues.each { |i| i.repository = repo } if repo.is_a? Repository
|
34
|
+
issues
|
35
|
+
end
|
36
|
+
|
37
|
+
# 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
|
+
|
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
|
+
validate_args(user => :user, repo => :repo)
|
58
|
+
super user, repo, number
|
59
|
+
end
|
60
|
+
|
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
|
66
|
+
issue
|
67
|
+
end
|
68
|
+
|
69
|
+
def reopen(*args)
|
70
|
+
data = @api.post(command_path("reopen"))
|
71
|
+
end
|
72
|
+
|
73
|
+
def close(*args)
|
74
|
+
data = @api.post(command_path("close"))
|
75
|
+
end
|
76
|
+
|
77
|
+
def save
|
78
|
+
data = @api.post(command_path("edit"), { :title => self.title, :body => self.body })
|
79
|
+
end
|
80
|
+
|
81
|
+
%w(add remove).each do |oper|
|
82
|
+
define_method("#{oper}_label") do |*labels|
|
83
|
+
labels.each do |label|
|
84
|
+
@api.post("#{prefix("label/#{oper}")}/#{label}/#{number}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def comment(comment)
|
90
|
+
@api.post(command_path("comment"), { :comment => comment })
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def prefix(command)
|
95
|
+
"/issues/#{command}/#{repository.owner}/#{repository.name}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def command_path(command)
|
99
|
+
"#{prefix(command)}/#{number}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/octopi/key.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Key < Base
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
attr_reader :user
|
6
|
+
|
7
|
+
def initialize(api, data, user = nil)
|
8
|
+
super api, data
|
9
|
+
@user = user
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove!
|
13
|
+
result = @api.post "/user/key/remove", :id => id
|
14
|
+
keys = result["public_keys"].select { |k| k["title"] == title }
|
15
|
+
keys.empty?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Repository < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "repository", "repositories"
|
5
|
+
|
6
|
+
create_path "/repos/create"
|
7
|
+
find_path "/repos/search/:query"
|
8
|
+
resource_path "/repos/show/:id"
|
9
|
+
delete_path "/repos/delete/:id"
|
10
|
+
|
11
|
+
attr_accessor :private
|
12
|
+
|
13
|
+
# Returns all branches for the Repository
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# repo = Repository.find("fcoury", "octopi")
|
17
|
+
# repo.branches.each { |r| puts r.name }
|
18
|
+
#
|
19
|
+
def branches
|
20
|
+
Branch.find(self.owner, self.name,api)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns all tags for the Repository
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
# repo = Repository.find("fcoury", "octopi")
|
27
|
+
# repo.tags.each { |t| puts t.name }
|
28
|
+
#
|
29
|
+
def tags
|
30
|
+
Tag.find(self.owner, self.name)
|
31
|
+
end
|
32
|
+
|
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"
|
38
|
+
end
|
39
|
+
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)
|
45
|
+
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
|
57
|
+
|
58
|
+
self.validate_args(user => :user, repo => :repo)
|
59
|
+
super user, repo, api
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.find_all(*args)
|
63
|
+
# FIXME: This should be URI escaped, but have to check how the API
|
64
|
+
# handles escaped characters first.
|
65
|
+
super args.join(" ").gsub(/ /,'+')
|
66
|
+
end
|
67
|
+
|
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)
|
74
|
+
end
|
75
|
+
|
76
|
+
def commits(branch = "master")
|
77
|
+
api = self.api || ANONYMOUS_API
|
78
|
+
Commit.find_all(self, {:branch => branch}, api)
|
79
|
+
end
|
80
|
+
|
81
|
+
def issues(state = "open")
|
82
|
+
Issue.find_all(self, :state => state)
|
83
|
+
end
|
84
|
+
|
85
|
+
def all_issues
|
86
|
+
Issue::STATES.map{|state| self.issues(state)}.flatten
|
87
|
+
end
|
88
|
+
|
89
|
+
def issue(number)
|
90
|
+
Issue.find(self, number)
|
91
|
+
end
|
92
|
+
|
93
|
+
def collaborators
|
94
|
+
property('collaborators', [self.owner,self.name].join('/')).values
|
95
|
+
end
|
96
|
+
|
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)
|
103
|
+
end
|
104
|
+
|
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?
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,75 @@
|
|
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
|
+
api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
|
41
|
+
args = args.join('/') if args.is_a? Array
|
42
|
+
result = api.find(path_for(:resource), @resource_name[:singular], args)
|
43
|
+
key = result.keys.first
|
44
|
+
|
45
|
+
if result[key].is_a? Array
|
46
|
+
result[key].map { |r| new(api, r) }
|
47
|
+
else
|
48
|
+
Resource.for(key).new(api, result[key])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_all(*s)
|
53
|
+
api = s.last.is_a?(Api) ? s.pop : ANONYMOUS_API
|
54
|
+
find_plural(s, :find, api)
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_plural(s, path, api = ANONYMOUS_API)
|
58
|
+
s = s.join('/') if s.is_a? Array
|
59
|
+
api.find_all(path_for(path), @resource_name[:plural], s).
|
60
|
+
map do |item|
|
61
|
+
payload = block_given? ? yield(item) : item
|
62
|
+
new(api, payload)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def declassify(s)
|
67
|
+
(s.split('::').last || '').downcase if s
|
68
|
+
end
|
69
|
+
|
70
|
+
def path_for(type)
|
71
|
+
@path_spec[type]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/octopi/tag.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Tag < Base
|
3
|
+
include Resource
|
4
|
+
set_resource_name "tag"
|
5
|
+
|
6
|
+
resource_path "/repos/show/:id"
|
7
|
+
|
8
|
+
def self.find(user, repo)
|
9
|
+
user = user.login if user.is_a? User
|
10
|
+
repo = repo.name if repo.is_a? Repository
|
11
|
+
self.validate_args(user => :user, repo => :repo)
|
12
|
+
find_plural([user,repo,'tags'], :resource){
|
13
|
+
|i| {:name => i.first, :hash => i.last }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/octopi/user.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Octopi
|
2
|
+
class User < Base
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
find_path "/user/search/:query"
|
6
|
+
resource_path "/user/show/:id"
|
7
|
+
|
8
|
+
# Finds a single user identified by the given username
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# user = User.find("fcoury")
|
13
|
+
# puts user.login # should return 'fcoury'
|
14
|
+
def self.find(username)
|
15
|
+
self.validate_args(username => :user)
|
16
|
+
super username
|
17
|
+
end
|
18
|
+
|
19
|
+
# Finds all users whose username matches a given string
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# User.find_all("oe") # Matches joe, moe and monroe
|
24
|
+
#
|
25
|
+
def self.find_all(username)
|
26
|
+
self.validate_args(username => :user)
|
27
|
+
super username
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a collection of Repository objects, containing
|
31
|
+
# all repositories of the user.
|
32
|
+
#
|
33
|
+
# If user is the current authenticated user, some
|
34
|
+
# additional information will be provided for the
|
35
|
+
# Repositories.
|
36
|
+
def repositories
|
37
|
+
api = self.api || ANONYMOUS_API
|
38
|
+
Repository.find_by_user(login,api)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Searches for user Repository identified by
|
42
|
+
# name
|
43
|
+
def repository(name)
|
44
|
+
self.class.validate_args(name => :repo)
|
45
|
+
Repository.find(login, name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_repository(name, opts = {})
|
49
|
+
self.class.validate_args(name => :repo)
|
50
|
+
Repository.create(self, name, opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Adds an SSH Public Key to the user. Requires
|
54
|
+
# authentication.
|
55
|
+
def add_key(title, key)
|
56
|
+
raise APIError,
|
57
|
+
"To add a key, you must be authenticated" if @api.read_only?
|
58
|
+
|
59
|
+
result = @api.post("/user/key/add", :title => title, :key => key)
|
60
|
+
return if !result["public_keys"]
|
61
|
+
key_params = result["public_keys"].select { |k| k["title"] == title }
|
62
|
+
return if !key_params or key_params.empty?
|
63
|
+
Key.new(@api, key_params.first, self)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a list of Key objects containing all SSH Public Keys this user
|
67
|
+
# currently has. Requires authentication.
|
68
|
+
def keys
|
69
|
+
raise APIError,
|
70
|
+
"To add a key, you must be authenticated" if @api.read_only?
|
71
|
+
|
72
|
+
result = @api.get("/user/keys")
|
73
|
+
return unless result and result["public_keys"]
|
74
|
+
result["public_keys"].inject([]) { |result, element| result << Key.new(@api, element) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# takes one param, deep that indicates if returns
|
78
|
+
# only the user login or an user object
|
79
|
+
%w[followers following].each do |method|
|
80
|
+
define_method(method) do
|
81
|
+
user_property(method, false)
|
82
|
+
end
|
83
|
+
define_method("#{method}!") do
|
84
|
+
user_property(method, true)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def user_property(property, deep)
|
89
|
+
users = []
|
90
|
+
property(property, login).each_pair do |k,v|
|
91
|
+
return v unless deep
|
92
|
+
|
93
|
+
v.each { |u| users << User.find(u) }
|
94
|
+
end
|
95
|
+
|
96
|
+
users
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|