stephencelis-ghi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +5 -0
- data/Manifest.txt +14 -0
- data/README.rdoc +28 -0
- data/bin/ghi +4 -0
- data/lib/ghi.rb +11 -0
- data/lib/ghi/api.rb +84 -0
- data/lib/ghi/cli.rb +214 -0
- data/lib/ghi/issue.rb +15 -0
- data/spec/ghi/api_spec.rb +13 -0
- data/spec/ghi/cli_spec.rb +7 -0
- data/spec/ghi/issue_spec.rb +7 -0
- data/spec/ghi_spec.rb +14 -0
- metadata +68 -0
data/History.rdoc
ADDED
data/Manifest.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
= ghi
|
2
|
+
|
3
|
+
http://github.com/stephencelis/ghi
|
4
|
+
|
5
|
+
|
6
|
+
GitHub Issues on the command line. Use your `$EDITOR`, not your browser.
|
7
|
+
|
8
|
+
== HOW?
|
9
|
+
|
10
|
+
Get:
|
11
|
+
|
12
|
+
% gem install stephencelis-ghi --source=http://gems.github.com
|
13
|
+
|
14
|
+
|
15
|
+
Set (http://github.com/blog/180-local-github-config):
|
16
|
+
|
17
|
+
% git config --global github.user username
|
18
|
+
% git config --global github.token 6ef8395fecf207165f1a82178ae1b984
|
19
|
+
|
20
|
+
|
21
|
+
Go:
|
22
|
+
|
23
|
+
% ghi
|
24
|
+
Usage: ghi [options]
|
25
|
+
-l, --list, --show [number]
|
26
|
+
-o, --open, --reopen [number]
|
27
|
+
-c, --closed, --close [number]
|
28
|
+
-e, --edit [number]
|
data/bin/ghi
ADDED
data/lib/ghi.rb
ADDED
data/lib/ghi/api.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
class GHI::API
|
5
|
+
class InvalidConnection < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class ResponseError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
API_URL = "http://github.com/api/v2/yaml/issues/:action/:user/:repo"
|
12
|
+
|
13
|
+
attr_reader :user, :repo
|
14
|
+
|
15
|
+
def initialize(user, repo)
|
16
|
+
raise InvalidConnection if user.nil? || repo.nil?
|
17
|
+
@user, @repo = user, repo
|
18
|
+
end
|
19
|
+
|
20
|
+
def list(state = :open)
|
21
|
+
res = get :list, state
|
22
|
+
raise ResponseError, res if res["issues"].nil?
|
23
|
+
res["issues"].map { |attrs| GHI::Issue.new(attrs) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def show(number)
|
27
|
+
res = get :show, number
|
28
|
+
raise ResponseError, res if res["issue"].nil?
|
29
|
+
GHI::Issue.new res["issue"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def open(title, body)
|
33
|
+
res = post(:open, :title => title, :body => body)
|
34
|
+
raise ResponseError, res if res["issue"].nil?
|
35
|
+
GHI::Issue.new res["issue"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def edit(number, title, body)
|
39
|
+
res = post(:edit, number, :title => title, :body => body)
|
40
|
+
raise ResponseError, res if res["issue"].nil?
|
41
|
+
GHI::Issue.new res["issue"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def close(number)
|
45
|
+
res = post :close, number
|
46
|
+
raise ResponseError, res if res["issue"].nil?
|
47
|
+
GHI::Issue.new res["issue"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def reopen(number)
|
51
|
+
res = post :reopen, number
|
52
|
+
raise ResponseError, res if res["issue"].nil?
|
53
|
+
GHI::Issue.new res["issue"]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def get(*args)
|
59
|
+
res = Net::HTTP.get URI.parse(url(*args) + auth(true))
|
60
|
+
YAML.load res
|
61
|
+
end
|
62
|
+
|
63
|
+
def post(*args)
|
64
|
+
params = args.last.is_a?(Hash) ? args.pop : {}
|
65
|
+
params.update auth
|
66
|
+
res = Net::HTTP.post_form URI.parse(url(*args)), params
|
67
|
+
YAML.load res.body
|
68
|
+
end
|
69
|
+
|
70
|
+
def auth(query = false)
|
71
|
+
if query
|
72
|
+
"?login=#{GHI.login}&token=#{GHI.token}"
|
73
|
+
else
|
74
|
+
{ :login => GHI.login, :token => GHI.token }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def url(action, option = nil)
|
79
|
+
@url ||= API_URL.sub(":user", user).sub(":repo", repo)
|
80
|
+
uri = @url.sub ":action", action.to_s
|
81
|
+
uri += "/#{option}" unless option.nil?
|
82
|
+
uri
|
83
|
+
end
|
84
|
+
end
|
data/lib/ghi/cli.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require "tempfile"
|
3
|
+
require "ghi"
|
4
|
+
require "ghi/api"
|
5
|
+
require "ghi/issue"
|
6
|
+
|
7
|
+
class GHI::CLI
|
8
|
+
def initialize
|
9
|
+
`git config --get remote.origin.url`.match %r{([^:/]+)/([^/]+).git$}
|
10
|
+
@api = GHI::API.new *(@user, @repo = $1, $2)
|
11
|
+
|
12
|
+
option_parser.parse!(ARGV)
|
13
|
+
case options[:action]
|
14
|
+
when :list then list(options[:state])
|
15
|
+
when :show then show(options[:number])
|
16
|
+
when :open then open(options[:title])
|
17
|
+
when :edit then edit(options[:number])
|
18
|
+
when :close then close(options[:number])
|
19
|
+
when :reopen then reopen(options[:number])
|
20
|
+
else puts option_parser
|
21
|
+
end
|
22
|
+
rescue GHI::API::InvalidConnection
|
23
|
+
warn "#{File.basename $0}: not a GitHub repo"
|
24
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
25
|
+
warn "#{File.basename $0}: #{e.message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def options
|
31
|
+
@options ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def option_parser
|
35
|
+
@option_parser ||= OptionParser.new { |opts|
|
36
|
+
opts.banner = "Usage: #{File.basename $0} [options]"
|
37
|
+
|
38
|
+
opts.on("-l", "--list", "--show [number]") do |v|
|
39
|
+
options[:action] = :list
|
40
|
+
case v
|
41
|
+
when nil, /^o/
|
42
|
+
options[:state] = :open
|
43
|
+
when /^\d+$/
|
44
|
+
options[:action] = :show
|
45
|
+
options[:number] = v.to_i
|
46
|
+
when /^c/
|
47
|
+
options[:state] = :closed
|
48
|
+
else
|
49
|
+
raise OptionParser::InvalidOption
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("-o", "--open", "--reopen [number]") do |v|
|
54
|
+
options[:action] = :open
|
55
|
+
case v
|
56
|
+
when /^\d+$/
|
57
|
+
options[:action] = :reopen
|
58
|
+
options[:number] = v.to_i
|
59
|
+
when /^l/
|
60
|
+
options[:action] = :list
|
61
|
+
options[:state] = :open
|
62
|
+
else
|
63
|
+
options[:title] = v
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on("-c", "--closed", "--close [number]") do |v|
|
68
|
+
case v
|
69
|
+
when /^\d+$/
|
70
|
+
options[:action] = :close
|
71
|
+
options[:number] = v.to_i
|
72
|
+
when /^l/
|
73
|
+
options[:action] = :list
|
74
|
+
options[:state] = :closed
|
75
|
+
else
|
76
|
+
raise OptionParser::InvalidOption
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on("-e", "--edit [number]") do |v|
|
81
|
+
case v
|
82
|
+
when /^\d+$/
|
83
|
+
options[:action] = :edit
|
84
|
+
options[:state] = :closed
|
85
|
+
options[:number] = v.to_i
|
86
|
+
else
|
87
|
+
raise OptionParser::MissingArgument
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on("-V", "--version") do
|
92
|
+
puts "#{File.basename($0)}: v#{GHI::VERSION}"
|
93
|
+
exit
|
94
|
+
end
|
95
|
+
|
96
|
+
opts.on("-h", "--help") do
|
97
|
+
puts opts
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def list(state)
|
104
|
+
issues = @api.list state
|
105
|
+
puts "# #{state.to_s.capitalize} issues on #@user/#@repo"
|
106
|
+
if issues.empty?
|
107
|
+
puts "none"
|
108
|
+
else
|
109
|
+
puts issues.map { |i| " #{i.number.to_s.rjust(3)}: #{i.title[0,72]}" }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def show(number)
|
114
|
+
issue = @api.show number
|
115
|
+
puts <<-BODY
|
116
|
+
#{issue.number}: #{issue.title} [#{issue.state}]
|
117
|
+
|
118
|
+
votes: #{issue.votes}
|
119
|
+
created_at: #{issue.created_at}
|
120
|
+
updated_at: #{issue.updated_at}
|
121
|
+
|
122
|
+
#{issue.body}
|
123
|
+
|
124
|
+
-- #{issue.user}
|
125
|
+
BODY
|
126
|
+
end
|
127
|
+
|
128
|
+
def open(title)
|
129
|
+
edit = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
|
130
|
+
temp = Tempfile.open("open-issue-")
|
131
|
+
temp.write <<-BODY
|
132
|
+
#{"#{title}\n" unless title.nil?}
|
133
|
+
# Please explain the issue. The first line will be used as the title.
|
134
|
+
# Lines with "#" will be ignored, and empty issues will not be filed.
|
135
|
+
# All line breaks will be honored in accordance with GFM:
|
136
|
+
#
|
137
|
+
# http://github.github.com/github-flavored-markdown
|
138
|
+
#
|
139
|
+
# On #@user/#@repo:
|
140
|
+
#
|
141
|
+
# user: #{GHI.login}
|
142
|
+
BODY
|
143
|
+
temp.rewind
|
144
|
+
system "#{edit} #{temp.path}"
|
145
|
+
lines = File.readlines(temp.path).find_all { |l| !l.match(/^#/) }
|
146
|
+
temp.close!
|
147
|
+
if lines.to_s =~ /\A\s*\Z/
|
148
|
+
warn "can't file empty issue"
|
149
|
+
exit 1
|
150
|
+
else
|
151
|
+
title = lines.shift.strip
|
152
|
+
body = lines.join.sub(/\b\n\b/, " ").strip
|
153
|
+
issue = @api.open title, body
|
154
|
+
puts " Opened issue #{issue.number}: #{issue.title[0,58]}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def edit(number)
|
159
|
+
edit = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
|
160
|
+
begin
|
161
|
+
temp = Tempfile.open("open-issue-")
|
162
|
+
issue = @api.show number
|
163
|
+
temp.write <<-BODY
|
164
|
+
#{issue.title}#{"\n\n" + issue.body unless issue.body.to_s.strip == ""}
|
165
|
+
# Please explain the issue. The first line will be used as the title.
|
166
|
+
# Lines with "#" will be ignored, and empty issues will not be filed.
|
167
|
+
# All line breaks will be honored in accordance with GFM:
|
168
|
+
#
|
169
|
+
# http://github.github.com/github-flavored-markdown
|
170
|
+
#
|
171
|
+
# On #@user/#@repo:
|
172
|
+
#
|
173
|
+
# number: #{issue.number}
|
174
|
+
# user: #{issue.user}
|
175
|
+
# votes: #{issue.votes}
|
176
|
+
# state: #{issue.state}
|
177
|
+
# created at: #{issue.created_at}
|
178
|
+
BODY
|
179
|
+
if issue.updated_at > issue.created_at
|
180
|
+
temp.write "# updated at: #{issue.updated_at}"
|
181
|
+
end
|
182
|
+
temp.rewind
|
183
|
+
system "#{edit} #{temp.path}"
|
184
|
+
lines = File.readlines(temp.path)
|
185
|
+
if temp.readlines == lines
|
186
|
+
warn "no change"
|
187
|
+
exit 1
|
188
|
+
else
|
189
|
+
lines.reject! { |l| l.match(/^#/) }
|
190
|
+
if lines.to_s =~ /\A\s*\Z/
|
191
|
+
warn "can't file empty issue"
|
192
|
+
exit 1
|
193
|
+
else
|
194
|
+
title = lines.shift.strip
|
195
|
+
body = lines.join.sub(/\b\n\b/, " ").strip
|
196
|
+
issue = @api.edit(number, title, body)
|
197
|
+
puts " Updated issue #{issue.number}: #{issue.title[0,58]}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
ensure
|
201
|
+
temp.close!
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def close(number)
|
206
|
+
issue = @api.close number
|
207
|
+
puts " Closed issue #{issue.number}: #{issue.title[0,58]}"
|
208
|
+
end
|
209
|
+
|
210
|
+
def reopen(number)
|
211
|
+
issue = @api.reopen number
|
212
|
+
puts " Reopened issue #{issue.number}: #{issue.title[0,56]}"
|
213
|
+
end
|
214
|
+
end
|
data/lib/ghi/issue.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class GHI::Issue
|
2
|
+
attr_reader :number, :title, :body, :votes, :state, :user, :created_at,
|
3
|
+
:updated_at
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@number = options["number"]
|
7
|
+
@title = options["title"]
|
8
|
+
@body = options["body"]
|
9
|
+
@votes = options["votes"]
|
10
|
+
@state = options["state"]
|
11
|
+
@user = options["user"]
|
12
|
+
@created_at = options["created_at"]
|
13
|
+
@updated_at = options["updated_at"]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + "/../lib")
|
2
|
+
require "ghi"
|
3
|
+
require "ghi/api"
|
4
|
+
include GHI
|
5
|
+
|
6
|
+
describe GHI::API do
|
7
|
+
it "should require user and repo" do
|
8
|
+
proc { API.new(nil, nil) }.should raise_error(API::InvalidConnection)
|
9
|
+
proc { API.new("u", nil) }.should raise_error(API::InvalidConnection)
|
10
|
+
proc { API.new(nil, "r") }.should raise_error(API::InvalidConnection)
|
11
|
+
proc { API.new("u", "r") }.should_not raise_error(API::InvalidConnection)
|
12
|
+
end
|
13
|
+
end
|
data/spec/ghi_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + "/../lib")
|
2
|
+
require "ghi"
|
3
|
+
|
4
|
+
describe GHI do
|
5
|
+
it "should return login" do
|
6
|
+
GHI.stub!(:`).and_return "stephencelis\n"
|
7
|
+
GHI.login.should == "stephencelis"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return token" do
|
11
|
+
GHI.stub!(:`).and_return "da39a3ee5e6b4b0d3255bfef95601890\n"
|
12
|
+
GHI.token.should == "da39a3ee5e6b4b0d3255bfef95601890"
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stephencelis-ghi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Celis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-20 00:00:00 -07:00
|
13
|
+
default_executable: ghi
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: GitHub Issues on the command line. Use your `$EDITOR`, not your browser.
|
17
|
+
email:
|
18
|
+
- stephen@stephencelis.com
|
19
|
+
executables:
|
20
|
+
- ghi
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- History.rdoc
|
25
|
+
- Manifest.txt
|
26
|
+
- README.rdoc
|
27
|
+
files:
|
28
|
+
- History.rdoc
|
29
|
+
- Manifest.txt
|
30
|
+
- README.rdoc
|
31
|
+
- bin/ghi
|
32
|
+
- lib/ghi/api.rb
|
33
|
+
- lib/ghi/cli.rb
|
34
|
+
- lib/ghi/issue.rb
|
35
|
+
- lib/ghi.rb
|
36
|
+
- spec/ghi/api_spec.rb
|
37
|
+
- spec/ghi/cli_spec.rb
|
38
|
+
- spec/ghi/issue_spec.rb
|
39
|
+
- spec/ghi_spec.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/stephencelis/ghi
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --main
|
45
|
+
- README.rdoc
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project: ghi
|
63
|
+
rubygems_version: 1.2.0
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: GitHub Issues on the command line
|
67
|
+
test_files: []
|
68
|
+
|