octopi 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/.yardoc +0 -0
- data/README.rdoc +16 -41
- data/Rakefile +9 -0
- data/VERSION.yml +2 -2
- data/examples/overall.rb +1 -1
- data/lib/ext/hash_ext.rb +5 -0
- data/lib/ext/string_ext.rb +5 -0
- data/lib/octopi.rb +101 -202
- data/lib/octopi/api.rb +209 -0
- data/lib/octopi/base.rb +42 -38
- data/lib/octopi/blob.rb +12 -8
- data/lib/octopi/branch.rb +20 -7
- data/lib/octopi/branch_set.rb +11 -0
- data/lib/octopi/comment.rb +20 -0
- data/lib/octopi/commit.rb +39 -35
- data/lib/octopi/error.rb +17 -5
- data/lib/octopi/file_object.rb +6 -5
- data/lib/octopi/gist.rb +28 -0
- data/lib/octopi/issue.rb +49 -40
- data/lib/octopi/issue_comment.rb +7 -0
- data/lib/octopi/issue_set.rb +21 -0
- data/lib/octopi/key.rb +14 -7
- data/lib/octopi/key_set.rb +14 -0
- data/lib/octopi/plan.rb +5 -0
- data/lib/octopi/repository.rb +66 -45
- data/lib/octopi/repository_set.rb +9 -0
- data/lib/octopi/resource.rb +11 -16
- data/lib/octopi/self.rb +33 -0
- data/lib/octopi/tag.rb +12 -6
- data/lib/octopi/user.rb +62 -38
- data/octopi.gemspec +43 -12
- data/test/api_test.rb +58 -0
- data/test/authenticated_test.rb +39 -0
- data/test/blob_test.rb +23 -0
- data/test/branch_test.rb +20 -0
- data/test/commit_test.rb +82 -0
- data/test/file_object_test.rb +39 -0
- data/test/gist_test.rb +16 -0
- data/test/issue_comment.rb +19 -0
- data/test/issue_set_test.rb +33 -0
- data/test/issue_test.rb +120 -0
- data/test/key_set_test.rb +29 -0
- data/test/key_test.rb +35 -0
- data/test/repository_set_test.rb +23 -0
- data/test/repository_test.rb +141 -0
- data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
- data/test/tag_test.rb +20 -0
- data/test/test_helper.rb +236 -0
- data/test/user_test.rb +92 -0
- metadata +54 -12
- data/examples/github.yml.example +0 -14
- 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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
data/lib/octopi/file_object.rb
CHANGED
@@ -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(
|
9
|
-
|
10
|
-
repo
|
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
|
data/lib/octopi/gist.rb
ADDED
@@ -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
|
-
|
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(
|
26
|
-
|
27
|
-
user,
|
28
|
-
state =
|
29
|
-
|
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,
|
33
|
-
issues.each { |i| i.repository = repo }
|
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(
|
39
|
-
|
40
|
-
|
41
|
-
|
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(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
issue
|
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
|
-
|
70
|
-
|
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
|
74
|
-
data =
|
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 =
|
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
|
-
|
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
|
-
|
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,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
|
8
|
-
|
9
|
-
|
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 =
|
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
|
data/lib/octopi/plan.rb
ADDED
data/lib/octopi/repository.rb
CHANGED
@@ -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.
|
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.
|
31
|
-
end
|
39
|
+
Tag.all(:user => self.owner, :repo => self)
|
40
|
+
end
|
32
41
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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("<", "<").gsub(">", ">")
|
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
|
42
|
-
|
43
|
-
self.
|
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(
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
user = user.
|
53
|
-
|
54
|
-
|
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
|
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
|
-
|
69
|
-
|
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
|
-
|
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(
|
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(
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
106
|
-
|
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
|