octopi 0.1.0 → 0.2.1
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 +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
|