stephencelis-ghi 0.0.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/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
|
+
|