ghi 0.3.1 → 0.9.0.dev
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/bin/ghi +2 -4
- data/lib/ghi.rb +112 -42
- data/lib/ghi/authorization.rb +71 -0
- data/lib/ghi/client.rb +126 -0
- data/lib/ghi/commands.rb +20 -0
- data/lib/ghi/commands/assign.rb +53 -0
- data/lib/ghi/commands/close.rb +45 -0
- data/lib/ghi/commands/command.rb +114 -0
- data/lib/ghi/commands/comment.rb +104 -0
- data/lib/ghi/commands/config.rb +35 -0
- data/lib/ghi/commands/edit.rb +49 -0
- data/lib/ghi/commands/help.rb +62 -0
- data/lib/ghi/commands/label.rb +153 -0
- data/lib/ghi/commands/list.rb +133 -0
- data/lib/ghi/commands/milestone.rb +150 -0
- data/lib/ghi/commands/open.rb +69 -0
- data/lib/ghi/commands/show.rb +21 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/formatting.rb +301 -0
- data/lib/ghi/formatting/colors.rb +295 -0
- data/lib/ghi/json.rb +1304 -0
- metadata +71 -49
- data/README.rdoc +0 -126
- data/lib/ghi/api.rb +0 -145
- data/lib/ghi/cli.rb +0 -657
- data/lib/ghi/issue.rb +0 -30
- data/spec/ghi/api_spec.rb +0 -218
- data/spec/ghi/cli_spec.rb +0 -267
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -62
metadata
CHANGED
@@ -1,70 +1,92 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: ghi
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: -1101288818
|
5
|
+
prerelease: 6
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 9
|
9
|
+
- 0
|
10
|
+
- dev
|
11
|
+
version: 0.9.0.dev
|
6
12
|
platform: ruby
|
7
|
-
authors:
|
13
|
+
authors:
|
8
14
|
- Stephen Celis
|
9
15
|
autorequire:
|
10
16
|
bindir: bin
|
11
17
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '2.0'
|
22
|
-
type: :development
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: *70100746900540
|
25
|
-
description: GitHub Issues on the command line. Use your `$EDITOR`, not your browser.
|
18
|
+
|
19
|
+
date: 2012-04-04 00:00:00 -07:00
|
20
|
+
default_executable:
|
21
|
+
dependencies: []
|
22
|
+
|
23
|
+
description: |
|
24
|
+
GitHub Issues on the command line. Use your `$EDITOR`, not your browser.
|
25
|
+
|
26
26
|
email: stephen@stephencelis.com
|
27
|
-
executables:
|
27
|
+
executables:
|
28
28
|
- ghi
|
29
29
|
extensions: []
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
- lib/ghi/
|
35
|
-
- lib/ghi/
|
36
|
-
- lib/ghi/
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/ghi/authorization.rb
|
35
|
+
- lib/ghi/client.rb
|
36
|
+
- lib/ghi/commands/assign.rb
|
37
|
+
- lib/ghi/commands/close.rb
|
38
|
+
- lib/ghi/commands/command.rb
|
39
|
+
- lib/ghi/commands/comment.rb
|
40
|
+
- lib/ghi/commands/config.rb
|
41
|
+
- lib/ghi/commands/edit.rb
|
42
|
+
- lib/ghi/commands/help.rb
|
43
|
+
- lib/ghi/commands/label.rb
|
44
|
+
- lib/ghi/commands/list.rb
|
45
|
+
- lib/ghi/commands/milestone.rb
|
46
|
+
- lib/ghi/commands/open.rb
|
47
|
+
- lib/ghi/commands/show.rb
|
48
|
+
- lib/ghi/commands/version.rb
|
49
|
+
- lib/ghi/commands.rb
|
50
|
+
- lib/ghi/formatting/colors.rb
|
51
|
+
- lib/ghi/formatting.rb
|
52
|
+
- lib/ghi/json.rb
|
37
53
|
- lib/ghi.rb
|
38
|
-
-
|
39
|
-
|
40
|
-
|
41
|
-
- spec/ghi_spec.rb
|
42
|
-
- README.rdoc
|
43
|
-
homepage: http://github.com/stephencelis/ghi
|
54
|
+
- bin/ghi
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: https://github.com/stephencelis/ghi
|
44
57
|
licenses: []
|
58
|
+
|
45
59
|
post_install_message:
|
46
|
-
rdoc_options:
|
47
|
-
|
48
|
-
|
49
|
-
require_paths:
|
60
|
+
rdoc_options: []
|
61
|
+
|
62
|
+
require_paths:
|
50
63
|
- lib
|
51
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
65
|
none: false
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
|
57
|
-
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
74
|
none: false
|
59
|
-
requirements:
|
60
|
-
- -
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
|
75
|
+
requirements:
|
76
|
+
- - ">"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 25
|
79
|
+
segments:
|
80
|
+
- 1
|
81
|
+
- 3
|
82
|
+
- 1
|
83
|
+
version: 1.3.1
|
63
84
|
requirements: []
|
85
|
+
|
64
86
|
rubyforge_project:
|
65
|
-
rubygems_version: 1.
|
87
|
+
rubygems_version: 1.6.2
|
66
88
|
signing_key:
|
67
89
|
specification_version: 3
|
68
90
|
summary: GitHub Issues command line interface
|
69
91
|
test_files: []
|
70
|
-
|
92
|
+
|
data/README.rdoc
DELETED
@@ -1,126 +0,0 @@
|
|
1
|
-
= ghi
|
2
|
-
|
3
|
-
http://github.com/stephencelis/ghi
|
4
|
-
|
5
|
-
|
6
|
-
GitHub Issues on the command line. Use your <tt>$EDITOR</tt>, not your
|
7
|
-
browser.
|
8
|
-
|
9
|
-
== HOW?
|
10
|
-
|
11
|
-
Get:
|
12
|
-
|
13
|
-
% gem install ghi
|
14
|
-
|
15
|
-
|
16
|
-
Go:
|
17
|
-
|
18
|
-
Usage: ghi [options]
|
19
|
-
-l, --list [state|term|number]
|
20
|
-
--search, --show
|
21
|
-
-v, --verbose
|
22
|
-
--ssl
|
23
|
-
-o, --open [title|number]
|
24
|
-
--reopen
|
25
|
-
-c, --closed, --close [number]
|
26
|
-
-e, --edit [number]
|
27
|
-
-r, --repo, --repository [name]
|
28
|
-
-m, --comment [number|comment]
|
29
|
-
-t, --label [number] [label]
|
30
|
-
--claim [number]
|
31
|
-
-d, --unlabel [number] [label]
|
32
|
-
-u, --url [state|number]
|
33
|
-
--[no-]color
|
34
|
-
--[no-]pager
|
35
|
-
-V, --version
|
36
|
-
-h, --help
|
37
|
-
|
38
|
-
|
39
|
-
== EXAMPLE?
|
40
|
-
|
41
|
-
ghi works simply from within a repository. Some short examples:
|
42
|
-
|
43
|
-
ghi -l # Lists all open issues
|
44
|
-
ghi # Shorter shorthand for "ghi -l"
|
45
|
-
ghi -v # Lists all open issues, verbosely (includes body)
|
46
|
-
ghi -lc # Lists all closed issues
|
47
|
-
ghi -l "doesn't work" # Searches for open issues matching "doesn't work"
|
48
|
-
ghi -l invalid -c # Searches for closed issues matching "invalid"
|
49
|
-
ghi -l1 # Shows issue 1
|
50
|
-
ghi -1 # Shorter shorthand for "ghi -l1"
|
51
|
-
ghi 1 # Shorter shorthand still
|
52
|
-
ghi -o # Opens a new issue (in your $EDITOR)
|
53
|
-
ghi -o "New issue" # Opens a new issue with the title "New issue"
|
54
|
-
ghi -o "Title" -m "Body" # Opens a new issue with specified title and body
|
55
|
-
ghi -e1 # Edits issue number 1 (in your $EDITOR)
|
56
|
-
ghi -e1 -m "New body" # Edits issue number 1 with the specified body
|
57
|
-
ghi -c1 # Closes issue 1
|
58
|
-
ghi -c1 -m # Closes issue with comment (from your $EDITOR)
|
59
|
-
ghi -c1 -m "Comment" # Closes issue with specified comment
|
60
|
-
ghi -o1 # Reopens 1 (accepts comments, too)
|
61
|
-
ghi -m1 # Comments on issue 1 (in your $EDITOR)
|
62
|
-
ghi -t1 "tag" # Labels issue 1 with "tag"
|
63
|
-
ghi -d1 "tag" # Removes the label, "tag"
|
64
|
-
ghi --claim 1 # Tags issue 1 with your GitHub username
|
65
|
-
ghi -u # Loads issues in your browser.
|
66
|
-
ghi -u1 # Loads an issue in your browser.
|
67
|
-
|
68
|
-
|
69
|
-
ghi also works anywhere:
|
70
|
-
|
71
|
-
ghi -rghi # Your fork of "ghi"
|
72
|
-
ghi -rstephencelis/ghi # Mine: "stephencelis/ghi"
|
73
|
-
ghi stephencelis/ghi # Shorthand to merely list open.
|
74
|
-
|
75
|
-
|
76
|
-
ghi uses ANSI colors if you use them in git.
|
77
|
-
|
78
|
-
ghi looks for a <tt>$GHI_PAGER</tt> variable for paging.
|
79
|
-
|
80
|
-
Always favor SSL by setting it:
|
81
|
-
|
82
|
-
git config --global github.ssl true
|
83
|
-
|
84
|
-
|
85
|
-
== CONTRIBUTORS
|
86
|
-
|
87
|
-
* Jamie Macey (http://blog.tracefunc.com)
|
88
|
-
* Hiroshi Nakamura (http://github.com/nahi)
|
89
|
-
* David J. Hamilton
|
90
|
-
|
91
|
-
|
92
|
-
=== CONTRIBUTE?
|
93
|
-
|
94
|
-
Running the tests should be as simple as a `bundle` and
|
95
|
-
`bundle exec spec spec`.
|
96
|
-
|
97
|
-
ghi is not under currently under the control of any gem packaging system. To
|
98
|
-
build, use RubyGems:
|
99
|
-
|
100
|
-
% gem build ghi.gemspec
|
101
|
-
% sudo gem install ghi*.gem
|
102
|
-
|
103
|
-
|
104
|
-
== LICENSE
|
105
|
-
|
106
|
-
(The MIT License)
|
107
|
-
|
108
|
-
(c) 2009-* Stephen Celis, stephen@stephencelis.com.
|
109
|
-
|
110
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
111
|
-
of this software and associated documentation files (the "Software"), to deal
|
112
|
-
in the Software without restriction, including without limitation the rights
|
113
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
114
|
-
copies of the Software, and to permit persons to whom the Software is
|
115
|
-
furnished to do so, subject to the following conditions:
|
116
|
-
|
117
|
-
The above copyright notice and this permission notice shall be included in all
|
118
|
-
copies or substantial portions of the Software.
|
119
|
-
|
120
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
121
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
122
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
123
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
124
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
125
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
126
|
-
SOFTWARE.
|
data/lib/ghi/api.rb
DELETED
@@ -1,145 +0,0 @@
|
|
1
|
-
require "net/https"
|
2
|
-
require "yaml"
|
3
|
-
require "cgi"
|
4
|
-
|
5
|
-
class GHI::API
|
6
|
-
class GHIError < StandardError
|
7
|
-
end
|
8
|
-
|
9
|
-
class InvalidRequest < GHIError
|
10
|
-
end
|
11
|
-
|
12
|
-
class InvalidConnection < GHIError
|
13
|
-
end
|
14
|
-
|
15
|
-
class ResponseError < GHIError
|
16
|
-
end
|
17
|
-
|
18
|
-
API_HOST = "github.com"
|
19
|
-
API_PATH = "/api/v2/yaml/issues/:action/:user/:repo"
|
20
|
-
|
21
|
-
attr_reader :user, :repo
|
22
|
-
|
23
|
-
def initialize(user, repo, use_ssl = false)
|
24
|
-
raise InvalidConnection if user.nil? || repo.nil?
|
25
|
-
@user, @repo, @use_ssl = user, repo, use_ssl
|
26
|
-
end
|
27
|
-
|
28
|
-
def search(term, state = :open)
|
29
|
-
get(:search, state, term)["issues"].map { |attrs| GHI::Issue.new(attrs) }
|
30
|
-
end
|
31
|
-
|
32
|
-
def list(state = :open)
|
33
|
-
get(:list, state)["issues"].map { |attrs| GHI::Issue.new(attrs) }
|
34
|
-
end
|
35
|
-
|
36
|
-
def show(number)
|
37
|
-
GHI::Issue.new get(:show, number)["issue"]
|
38
|
-
end
|
39
|
-
|
40
|
-
def comments(number)
|
41
|
-
get(:comments, number)["comments"]
|
42
|
-
end
|
43
|
-
|
44
|
-
def open(title, body)
|
45
|
-
if title.empty? && body.empty?
|
46
|
-
raise GHIError, "Aborting request due to empty issue."
|
47
|
-
end
|
48
|
-
GHI::Issue.new post(:open, "title" => title, "body" => body)["issue"]
|
49
|
-
end
|
50
|
-
|
51
|
-
def edit(number, title, body)
|
52
|
-
if title.empty? && body.empty?
|
53
|
-
raise GHIError, "Aborting request due to empty issue."
|
54
|
-
end
|
55
|
-
res = post :edit, number, "title" => title, "body" => body
|
56
|
-
GHI::Issue.new res["issue"]
|
57
|
-
end
|
58
|
-
|
59
|
-
def close(number)
|
60
|
-
GHI::Issue.new post(:close, number)["issue"]
|
61
|
-
end
|
62
|
-
|
63
|
-
def reopen(number)
|
64
|
-
GHI::Issue.new post(:reopen, number)["issue"]
|
65
|
-
end
|
66
|
-
|
67
|
-
def add_label(label, number)
|
68
|
-
post("label/add", label, number)["labels"]
|
69
|
-
end
|
70
|
-
|
71
|
-
def remove_label(label, number)
|
72
|
-
post("label/remove", label, number)["labels"]
|
73
|
-
end
|
74
|
-
|
75
|
-
def comment(number, comment)
|
76
|
-
if comment.empty?
|
77
|
-
raise GHIError, "Aborting request due to empty comment."
|
78
|
-
end
|
79
|
-
post(:comment, number, "comment" => comment)["comment"]
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
def get(*args)
|
85
|
-
res = nil
|
86
|
-
http = Net::HTTP.new(API_HOST, @use_ssl ? 443 : 80)
|
87
|
-
http.use_ssl = true if @use_ssl
|
88
|
-
http.start do
|
89
|
-
if @use_ssl
|
90
|
-
req = Net::HTTP::Post.new path(*args)
|
91
|
-
req.set_form_data auth
|
92
|
-
else
|
93
|
-
req = Net::HTTP::Get.new(path(*args) + auth(true))
|
94
|
-
end
|
95
|
-
res = YAML.load http.request(req).body
|
96
|
-
end
|
97
|
-
|
98
|
-
raise ResponseError, errors(res) if res["error"] || res[:error]
|
99
|
-
res
|
100
|
-
rescue ArgumentError, URI::InvalidURIError
|
101
|
-
raise ResponseError, "GitHub hiccuped on your request"
|
102
|
-
rescue SocketError
|
103
|
-
raise ResponseError, "couldn't find the internet"
|
104
|
-
end
|
105
|
-
|
106
|
-
def post(*args)
|
107
|
-
params = args.last.is_a?(Hash) ? args.pop : {}
|
108
|
-
|
109
|
-
res = nil
|
110
|
-
http = Net::HTTP.new(API_HOST, @use_ssl ? 443 : 80)
|
111
|
-
http.use_ssl = true if @use_ssl
|
112
|
-
http.start do
|
113
|
-
req = Net::HTTP::Post.new path(*args)
|
114
|
-
req.set_form_data params.merge(auth)
|
115
|
-
res = YAML.load http.request(req).body
|
116
|
-
end
|
117
|
-
|
118
|
-
raise ResponseError, errors(res) if res["error"] || res[:error]
|
119
|
-
res
|
120
|
-
rescue ArgumentError, URI::InvalidURIError
|
121
|
-
raise ResponseError, "GitHub hiccuped on your request"
|
122
|
-
rescue SocketError
|
123
|
-
raise ResponseError, "couldn't find the internet"
|
124
|
-
end
|
125
|
-
|
126
|
-
def errors(response)
|
127
|
-
return response[:error] if response.key? :error
|
128
|
-
[*response["error"]].map { |e| e["error"] } * ", "
|
129
|
-
end
|
130
|
-
|
131
|
-
def auth(query = false)
|
132
|
-
if query
|
133
|
-
"?login=#{GHI.login}&token=#{GHI.token}"
|
134
|
-
else
|
135
|
-
{ "login" => GHI.login, "token" => GHI.token }
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def path(action, *args)
|
140
|
-
@path ||= API_PATH.sub(":user", user).sub(":repo", repo)
|
141
|
-
path = @path.sub ":action", action.to_s
|
142
|
-
path << "/#{args.join("/")}" unless args.empty?
|
143
|
-
path
|
144
|
-
end
|
145
|
-
end
|
data/lib/ghi/cli.rb
DELETED
@@ -1,657 +0,0 @@
|
|
1
|
-
require "optparse"
|
2
|
-
require "tempfile"
|
3
|
-
require "ghi"
|
4
|
-
require "ghi/api"
|
5
|
-
require "ghi/issue"
|
6
|
-
|
7
|
-
begin
|
8
|
-
require "launchy"
|
9
|
-
rescue LoadError
|
10
|
-
# No launchy!
|
11
|
-
end
|
12
|
-
|
13
|
-
module GHI::CLI #:nodoc:
|
14
|
-
module FileHelper
|
15
|
-
def launch_editor(file)
|
16
|
-
system "#{editor} #{file.path}"
|
17
|
-
end
|
18
|
-
|
19
|
-
def gets_from_editor(issue)
|
20
|
-
if windows?
|
21
|
-
warn "Please supply the message with the -m option"
|
22
|
-
exit 1
|
23
|
-
end
|
24
|
-
|
25
|
-
if in_repo?
|
26
|
-
File.open message_path, "a+", &file_proc(issue)
|
27
|
-
else
|
28
|
-
Tempfile.open message_filename, &file_proc(issue)
|
29
|
-
end
|
30
|
-
|
31
|
-
return @message if comment?
|
32
|
-
return @message.shift.strip, @message.join.sub(/\b\n\b/, " ").strip
|
33
|
-
end
|
34
|
-
|
35
|
-
def delete_message
|
36
|
-
File.delete message_path
|
37
|
-
rescue Errno::ENOENT, TypeError
|
38
|
-
nil
|
39
|
-
end
|
40
|
-
|
41
|
-
def message_path
|
42
|
-
File.join gitdir, message_filename
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def editor
|
48
|
-
ENV["GHI_EDITOR"] || ENV["GIT_EDITOR"] ||
|
49
|
-
`git config --get-all core.editor`.split.first || ENV["EDITOR"] || "vi"
|
50
|
-
end
|
51
|
-
|
52
|
-
def gitdir
|
53
|
-
@gitdir ||= `git rev-parse --git-dir 2>/dev/null`.chomp
|
54
|
-
end
|
55
|
-
|
56
|
-
def message_filename
|
57
|
-
@message_filename ||= "GHI_#{action.to_s.upcase}#{number}_MESSAGE"
|
58
|
-
end
|
59
|
-
|
60
|
-
def file_proc(issue)
|
61
|
-
lambda do |file|
|
62
|
-
file << edit_format(issue).join("\n") if File.zero? file.path
|
63
|
-
file.rewind
|
64
|
-
launch_editor file
|
65
|
-
@message = File.readlines(file.path).find_all { |l| !l.match(/^#/) }
|
66
|
-
|
67
|
-
if message.to_s =~ /\A\s*\Z/
|
68
|
-
raise GHI::API::InvalidRequest, "can't file empty message"
|
69
|
-
end
|
70
|
-
raise GHI::API::InvalidRequest, "no change" if issue == message
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def in_repo?
|
75
|
-
!gitdir.empty? && user == local_user && repo == local_repo
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
module FormattingHelper
|
80
|
-
def list_header(term = nil)
|
81
|
-
if term
|
82
|
-
"# #{state.to_s.capitalize} #{term.inspect} issues on #{user}/#{repo}"
|
83
|
-
else
|
84
|
-
"# #{state.to_s.capitalize} issues on #{user}/#{repo}"
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def list_format(issues, verbosity = nil)
|
89
|
-
unless issues.empty?
|
90
|
-
if verbosity
|
91
|
-
issues.map { |i| ["=" * 79] + show_format(i) }
|
92
|
-
else
|
93
|
-
issues.map { |i| " #{i.number.to_s.rjust 3}: #{truncate(i.title, 72)} #{label_format(i.labels)}" }
|
94
|
-
end
|
95
|
-
else
|
96
|
-
"none"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def label_format(labels)
|
101
|
-
labels and labels.map {|l| "[#{l}]"}.join(' ')
|
102
|
-
end
|
103
|
-
|
104
|
-
def edit_format(issue)
|
105
|
-
l = []
|
106
|
-
l << issue.title if issue.title && !comment?
|
107
|
-
l << ""
|
108
|
-
l << issue.body if issue.body && !comment?
|
109
|
-
if comment?
|
110
|
-
l << "# Please enter your comment."
|
111
|
-
else
|
112
|
-
l << "# Please explain the issue. The first line will become the title."
|
113
|
-
end
|
114
|
-
l << "# Lines beginning '#' will be ignored; ghi aborts empty messages."
|
115
|
-
l << "# All line breaks will be honored in accordance with GFM:"
|
116
|
-
l << "#"
|
117
|
-
l << "# http://github.github.com/github-flavored-markdown"
|
118
|
-
l << "#"
|
119
|
-
l << "# On #{user}/#{repo}:"
|
120
|
-
l << "#"
|
121
|
-
l += show_format(issue, false).map { |line| "# #{line}" }
|
122
|
-
end
|
123
|
-
|
124
|
-
def show_format(issue, verbose = true)
|
125
|
-
l = []
|
126
|
-
l << " number: #{issue.number}" if issue.number
|
127
|
-
l << " state: #{issue.state}" if issue.state
|
128
|
-
l << " title: #{indent(issue.title, 15, 0).join}" if issue.title
|
129
|
-
l << " labels: #{label_format(issue.labels)}" unless issue.labels.nil? || issue.labels.empty?
|
130
|
-
l << " user: #{issue.user || GHI.login}"
|
131
|
-
l << " votes: #{issue.votes}" if issue.votes
|
132
|
-
l << " created at: #{issue.created_at}" if issue.created_at
|
133
|
-
l << " updated at: #{issue.updated_at}" if issue.updated_at
|
134
|
-
return l unless verbose
|
135
|
-
l << ""
|
136
|
-
l += indent(issue.body)[0..-2]
|
137
|
-
|
138
|
-
comments = api.comments(issue.number)
|
139
|
-
unless comments.empty?
|
140
|
-
l << "=" * 79
|
141
|
-
comments.each do |c|
|
142
|
-
l << "#{c["user"]} commented:"
|
143
|
-
l << ""
|
144
|
-
l += indent(c["body"])
|
145
|
-
l << "-" * 79
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
l
|
150
|
-
end
|
151
|
-
|
152
|
-
def action_format(value = nil)
|
153
|
-
key = "#{action.to_s.capitalize.sub(/e?$/, "ed")} issue #{number}"
|
154
|
-
"#{key}: #{truncate(value.to_s, 78 - key.length)}"
|
155
|
-
end
|
156
|
-
|
157
|
-
def truncate(string, length)
|
158
|
-
result = string.scan(/.{0,#{length - 3}}(?:\s|\Z)/).first.strip
|
159
|
-
result << "..." if result != string
|
160
|
-
result
|
161
|
-
end
|
162
|
-
|
163
|
-
def indent(string, level = 4, first = level)
|
164
|
-
string = string.gsub(/\n{3,}/, "\n\n")
|
165
|
-
lines = string.scan(/.{0,#{79 - level}}(?:\s|\Z)/).map { |line|
|
166
|
-
" " * level + line
|
167
|
-
}
|
168
|
-
lines.first.sub!(/^\s+/) {} if first != level
|
169
|
-
lines
|
170
|
-
end
|
171
|
-
|
172
|
-
private
|
173
|
-
|
174
|
-
def comment?
|
175
|
-
![:open, :edit].include?(action)
|
176
|
-
end
|
177
|
-
|
178
|
-
def puts(*args)
|
179
|
-
args = args.flatten.each { |arg|
|
180
|
-
arg.gsub!(/\b\*(.+)\*\b/) { "\e[1m#$1\e[0m" } # Bold
|
181
|
-
arg.gsub!(/\b_(.+)_\b/) { "\e[4m#$1\e[0m" } # Underline
|
182
|
-
arg.gsub!(/(state:)?(# Open.*| open)$/) { "#$1\e[32m#$2\e[0m" }
|
183
|
-
arg.gsub!(/(state:)?(# Closed.*| closed)$/) { "#$1\e[31m#$2\e[0m" }
|
184
|
-
marked = [GHI.login, search_term, tag, "(?:#|gh)-\d+"].compact * "|"
|
185
|
-
unless arg.include? "\e"
|
186
|
-
arg.gsub!(/(#{marked})/i) { "\e[1;4;33m#{$&}\e[0m" }
|
187
|
-
end
|
188
|
-
} if colorize?
|
189
|
-
rescue NoMethodError
|
190
|
-
# Do nothing.
|
191
|
-
ensure
|
192
|
-
$stdout.puts(*args)
|
193
|
-
end
|
194
|
-
|
195
|
-
def colorize?
|
196
|
-
return @colorize if defined? @colorize
|
197
|
-
@colorize = if $stdout.isatty && !windows?
|
198
|
-
!`git config --get-regexp color`.chomp.empty?
|
199
|
-
else
|
200
|
-
false
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
def prepare_stdout
|
205
|
-
return if @prepared || @no_pager || !$stdout.isatty || pager.nil?
|
206
|
-
colorize? # Check for colorization.
|
207
|
-
$stdout = pager
|
208
|
-
@prepared = true
|
209
|
-
end
|
210
|
-
|
211
|
-
def pager
|
212
|
-
return @pager if defined? @pager
|
213
|
-
pager = ENV["GHI_PAGER"] || ENV["GIT_PAGER"] ||
|
214
|
-
`git config --get-all core.pager`.split.first || ENV["PAGER"] ||
|
215
|
-
"less -EMRX"
|
216
|
-
|
217
|
-
@pager = IO.popen(pager, "w")
|
218
|
-
end
|
219
|
-
|
220
|
-
def windows?
|
221
|
-
RUBY_PLATFORM.include? "mswin"
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
class Executable
|
226
|
-
include FileHelper, FormattingHelper
|
227
|
-
|
228
|
-
attr_reader :message, :local_user, :local_repo, :user, :repo, :api,
|
229
|
-
:action, :search_term, :number, :title, :body, :tag, :args, :verbosity,
|
230
|
-
:use_ssl
|
231
|
-
|
232
|
-
def parse!(*argv)
|
233
|
-
@args, @argv = argv, argv.dup
|
234
|
-
|
235
|
-
remotes = `git config --get-regexp remote\..+\.url`.split /\n/
|
236
|
-
repo_expression = %r{([^:/]+)/([^/\s]+?)(?:\.git)?$}
|
237
|
-
if remote = remotes.find { |r| r.include? "github.com" }
|
238
|
-
remote.match repo_expression
|
239
|
-
@user, @repo = $1, $2
|
240
|
-
end
|
241
|
-
|
242
|
-
option_parser.parse!(*args)
|
243
|
-
|
244
|
-
if action.nil? && fallback_parsing(*args).nil?
|
245
|
-
puts option_parser
|
246
|
-
exit
|
247
|
-
end
|
248
|
-
rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
|
249
|
-
if fallback_parsing(*e.args).nil?
|
250
|
-
warn "#{File.basename $0}: #{e.message}"
|
251
|
-
puts option_parser
|
252
|
-
exit 1
|
253
|
-
end
|
254
|
-
rescue OptionParser::MissingArgument, OptionParser::AmbiguousOption => e
|
255
|
-
warn "#{File.basename $0}: #{e.message}"
|
256
|
-
puts option_parser
|
257
|
-
exit 1
|
258
|
-
ensure
|
259
|
-
run!
|
260
|
-
$stdout.close_write
|
261
|
-
end
|
262
|
-
|
263
|
-
def run!
|
264
|
-
@api = GHI::API.new user, repo, use_ssl
|
265
|
-
|
266
|
-
case action
|
267
|
-
when :search then search
|
268
|
-
when :list then list
|
269
|
-
when :show then show
|
270
|
-
when :open then open
|
271
|
-
when :edit then edit
|
272
|
-
when :close then close
|
273
|
-
when :reopen then reopen
|
274
|
-
when :comment then prepare_comment && comment
|
275
|
-
when :label, :claim then prepare_label && label
|
276
|
-
when :unlabel then prepare_label && unlabel
|
277
|
-
when :url then url
|
278
|
-
end
|
279
|
-
rescue GHI::API::InvalidConnection
|
280
|
-
if action
|
281
|
-
code = 1
|
282
|
-
warn "#{File.basename $0}: not a GitHub repo"
|
283
|
-
puts option_parser if args.flatten.empty?
|
284
|
-
exit 1
|
285
|
-
end
|
286
|
-
rescue GHI::API::InvalidRequest => e
|
287
|
-
warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
|
288
|
-
delete_message
|
289
|
-
exit 1
|
290
|
-
rescue GHI::API::ResponseError => e
|
291
|
-
warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
|
292
|
-
exit 1
|
293
|
-
rescue GHI::API::GHIError => e
|
294
|
-
warn e.message
|
295
|
-
exit 1
|
296
|
-
end
|
297
|
-
|
298
|
-
def commenting?
|
299
|
-
@commenting
|
300
|
-
end
|
301
|
-
|
302
|
-
def state
|
303
|
-
@state || :open
|
304
|
-
end
|
305
|
-
|
306
|
-
private
|
307
|
-
|
308
|
-
def option_parser
|
309
|
-
@option_parser ||= OptionParser.new { |opts|
|
310
|
-
opts.banner = "Usage: #{File.basename $0} [options]"
|
311
|
-
|
312
|
-
opts.on("-l", "--list", "--search", "--show [state|term|number]") do |v|
|
313
|
-
@action = :list
|
314
|
-
case v
|
315
|
-
when nil, /^o(?:pen)?$/
|
316
|
-
# Defaults.
|
317
|
-
when /^\d+$/
|
318
|
-
@action = :show
|
319
|
-
@number = v.to_i
|
320
|
-
when /^c(?:losed)?$/
|
321
|
-
@state = :closed
|
322
|
-
when /^[uw]$/
|
323
|
-
@action = :url
|
324
|
-
when /^v$/
|
325
|
-
@verbosity = true
|
326
|
-
else
|
327
|
-
@action = :search
|
328
|
-
@search_term = v
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
opts.on("-v", "--verbose") do |v|
|
333
|
-
if v
|
334
|
-
@action ||= :list
|
335
|
-
@verbosity = true
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
opts.on("--ssl") do |ssl|
|
340
|
-
@use_ssl = true
|
341
|
-
end
|
342
|
-
|
343
|
-
opts.on("-o", "--open", "--reopen [title|number]") do |v|
|
344
|
-
@action = :open
|
345
|
-
case v
|
346
|
-
when /^\d+$/
|
347
|
-
@action = :reopen
|
348
|
-
@number = v.to_i
|
349
|
-
when /^l$/
|
350
|
-
@action = :list
|
351
|
-
when /^m$/
|
352
|
-
@title = args * " "
|
353
|
-
when /^[uw]$/
|
354
|
-
@action = :url
|
355
|
-
else
|
356
|
-
@title = v
|
357
|
-
end
|
358
|
-
end
|
359
|
-
|
360
|
-
opts.on("-c", "--closed", "--close [number]") do |v|
|
361
|
-
case v
|
362
|
-
when /^\d+$/
|
363
|
-
@action = :close
|
364
|
-
@number = v.to_i unless v.nil?
|
365
|
-
when /^l$/
|
366
|
-
@action = :list
|
367
|
-
@state = :closed
|
368
|
-
when /^[uw]$/
|
369
|
-
@action = :url
|
370
|
-
@state = :closed
|
371
|
-
when nil
|
372
|
-
if @action.nil? || @number
|
373
|
-
@action = :close
|
374
|
-
else
|
375
|
-
@state = :closed
|
376
|
-
end
|
377
|
-
else
|
378
|
-
raise OptionParser::InvalidArgument
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
opts.on("-e", "--edit [number]") do |v|
|
383
|
-
case v
|
384
|
-
when /^\d+$/
|
385
|
-
@action = :edit
|
386
|
-
@number = v.to_i
|
387
|
-
when nil
|
388
|
-
raise OptionParser::MissingArgument
|
389
|
-
else
|
390
|
-
raise OptionParser::InvalidArgument
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
opts.on("-r", "--repo", "--repository [name]") do |v|
|
395
|
-
case v
|
396
|
-
when nil
|
397
|
-
raise OptionParser::MissingArgument
|
398
|
-
else
|
399
|
-
repo = v.split "/"
|
400
|
-
if repo.length == 1
|
401
|
-
if @repo && `git remote 2>/dev/null`[/^#{repo}$/]
|
402
|
-
repo << @repo
|
403
|
-
else
|
404
|
-
repo.unshift(GHI.login)
|
405
|
-
end
|
406
|
-
end
|
407
|
-
@user, @repo = repo
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
opts.on("-m", "--comment [number|comment]") do |v|
|
412
|
-
case v
|
413
|
-
when /^\d+$/, nil
|
414
|
-
@action ||= :comment
|
415
|
-
@number ||= v.to_i unless v.nil?
|
416
|
-
@commenting = true
|
417
|
-
else
|
418
|
-
@body = v
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
422
|
-
opts.on("-t", "--label [number] [label]") do |v|
|
423
|
-
raise OptionParser::MissingArgument if v.nil?
|
424
|
-
@action ||= :label
|
425
|
-
@number = v.to_i
|
426
|
-
end
|
427
|
-
|
428
|
-
opts.on("--claim [number]") do |v|
|
429
|
-
raise OptionParser::MissingArgument if v.nil?
|
430
|
-
@action = :claim
|
431
|
-
@number = v.to_i
|
432
|
-
@tag = GHI.login
|
433
|
-
end
|
434
|
-
|
435
|
-
opts.on("-d", "--unlabel [number] [label]") do |v|
|
436
|
-
@action = :unlabel
|
437
|
-
case v
|
438
|
-
when /^\d+$/
|
439
|
-
@number = v.to_i
|
440
|
-
when /^\w+$/
|
441
|
-
@tag = v
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
opts.on("-u", "-w", "--url", "--web [state|number]") do |v|
|
446
|
-
@action = :url
|
447
|
-
case v
|
448
|
-
when /^\d+$/
|
449
|
-
@number = v.to_i
|
450
|
-
when /^c(?:losed)?$/
|
451
|
-
@state = :closed
|
452
|
-
when /^u(?:nread)?$/
|
453
|
-
@state = :unread
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
opts.on("--[no-]color") do |v|
|
458
|
-
@colorize = v
|
459
|
-
end
|
460
|
-
|
461
|
-
opts.on("--[no-]pager") do |v|
|
462
|
-
@no_pager = (v == false)
|
463
|
-
end
|
464
|
-
|
465
|
-
opts.on_tail("-V", "--version") do
|
466
|
-
puts "#{File.basename($0)}: v#{GHI::VERSION}"
|
467
|
-
exit
|
468
|
-
end
|
469
|
-
|
470
|
-
opts.on_tail("-h", "--help") do
|
471
|
-
puts opts
|
472
|
-
exit
|
473
|
-
end
|
474
|
-
}
|
475
|
-
end
|
476
|
-
|
477
|
-
def search
|
478
|
-
prepare_stdout
|
479
|
-
puts list_header(search_term)
|
480
|
-
issues = api.search search_term, state
|
481
|
-
puts list_format(issues, verbosity)
|
482
|
-
end
|
483
|
-
|
484
|
-
def list
|
485
|
-
prepare_stdout
|
486
|
-
puts list_header
|
487
|
-
issues = api.list(state)
|
488
|
-
puts list_format(issues, verbosity)
|
489
|
-
end
|
490
|
-
|
491
|
-
def show
|
492
|
-
prepare_stdout
|
493
|
-
issue = api.show number
|
494
|
-
puts show_format(issue)
|
495
|
-
end
|
496
|
-
|
497
|
-
def open
|
498
|
-
if title.nil?
|
499
|
-
new_title, new_body = gets_from_editor GHI::Issue.new("title" => body)
|
500
|
-
elsif @commenting && body.nil?
|
501
|
-
new_title, new_body = gets_from_editor GHI::Issue.new("title" => title)
|
502
|
-
end
|
503
|
-
new_title ||= title
|
504
|
-
new_body ||= body
|
505
|
-
issue = api.open new_title, new_body
|
506
|
-
delete_message
|
507
|
-
@number = issue.number
|
508
|
-
puts action_format(issue.title)
|
509
|
-
end
|
510
|
-
|
511
|
-
def edit
|
512
|
-
shown = api.show number
|
513
|
-
new_title, new_body = gets_from_editor(shown) if body.nil?
|
514
|
-
new_title ||= shown.title
|
515
|
-
new_body ||= body
|
516
|
-
issue = api.edit number, new_title, new_body
|
517
|
-
delete_message
|
518
|
-
puts action_format(issue.title)
|
519
|
-
end
|
520
|
-
|
521
|
-
def close
|
522
|
-
raise GHI::API::InvalidRequest, "need a number" if number.nil?
|
523
|
-
issue = api.close number
|
524
|
-
if @commenting || new_body = body
|
525
|
-
new_body ||= gets_from_editor issue
|
526
|
-
comment = api.comment number, new_body
|
527
|
-
end
|
528
|
-
puts action_format(issue.title)
|
529
|
-
puts "(commented)" if comment
|
530
|
-
end
|
531
|
-
|
532
|
-
def reopen
|
533
|
-
issue = api.reopen number
|
534
|
-
if @commenting || new_body = body
|
535
|
-
new_body ||= gets_from_editor issue
|
536
|
-
comment = api.comment number, new_body
|
537
|
-
end
|
538
|
-
puts action_format(issue.title)
|
539
|
-
puts "(commented)" if comment
|
540
|
-
end
|
541
|
-
|
542
|
-
def prepare_label
|
543
|
-
@tag ||= (body || args * " ")
|
544
|
-
raise GHI::API::InvalidRequest, "need a label" if @tag.empty?
|
545
|
-
true
|
546
|
-
end
|
547
|
-
|
548
|
-
def label
|
549
|
-
labels = api.add_label tag, number
|
550
|
-
puts action_format
|
551
|
-
puts indent(labels.join(", "))
|
552
|
-
end
|
553
|
-
|
554
|
-
def unlabel
|
555
|
-
labels = api.remove_label tag, number
|
556
|
-
puts action_format
|
557
|
-
puts indent(labels.empty? ? "no labels" : labels.join(", "))
|
558
|
-
end
|
559
|
-
|
560
|
-
def prepare_comment
|
561
|
-
@body = args.flatten.first
|
562
|
-
@commenting = false unless body.nil?
|
563
|
-
true
|
564
|
-
end
|
565
|
-
|
566
|
-
def comment
|
567
|
-
@body ||= gets_from_editor api.show(number)
|
568
|
-
comment = api.comment(number, body)
|
569
|
-
delete_message
|
570
|
-
puts "(comment})"
|
571
|
-
end
|
572
|
-
|
573
|
-
def url
|
574
|
-
url = "https://github.com/#{user}/#{repo}/issues"
|
575
|
-
if number.nil?
|
576
|
-
url << "/#{state}" unless state == :open
|
577
|
-
else
|
578
|
-
url << "/#{number}"
|
579
|
-
end
|
580
|
-
defined?(Launchy) ? Launchy.open(url) : puts(url)
|
581
|
-
end
|
582
|
-
|
583
|
-
#-
|
584
|
-
# Because these are mere fallbacks, any options used earlier will muddle
|
585
|
-
# things: `ghi list` will work, `ghi list -c` will not.
|
586
|
-
#
|
587
|
-
# Argument parsing will have to better integrate with option parsing to
|
588
|
-
# overcome this.
|
589
|
-
#+
|
590
|
-
def fallback_parsing(*arguments)
|
591
|
-
arguments = arguments.flatten
|
592
|
-
case command = arguments.shift
|
593
|
-
when nil, "list"
|
594
|
-
@action = :list
|
595
|
-
if arg = arguments.shift
|
596
|
-
@state ||= arg.to_sym if %w(open closed).include? arg
|
597
|
-
@user, @repo = arg.split "/" if arg.count("/") == 1
|
598
|
-
end
|
599
|
-
when "search"
|
600
|
-
@action = :search
|
601
|
-
@search_term ||= arguments.shift
|
602
|
-
when "show", /^-?(\d+)$/
|
603
|
-
@action = :show
|
604
|
-
@number ||= ($1 || arguments.shift[/\d+/]).to_i
|
605
|
-
when "open"
|
606
|
-
if arguments.first =~ /^\d+$/
|
607
|
-
@action = :reopen
|
608
|
-
@number ||= arguments.shift[/\d+/].to_i
|
609
|
-
else
|
610
|
-
@action = :open
|
611
|
-
@title = arguments.join(' ')
|
612
|
-
end
|
613
|
-
when "edit"
|
614
|
-
@action = :edit
|
615
|
-
@number ||= arguments.shift[/\d+/].to_i
|
616
|
-
when "close"
|
617
|
-
@action = :close
|
618
|
-
@number ||= arguments.shift[/\d+/].to_i
|
619
|
-
when "reopen"
|
620
|
-
@action = :reopen
|
621
|
-
@number ||= arguments.shift[/\d+/].to_i
|
622
|
-
when "label"
|
623
|
-
@action = :label
|
624
|
-
@number ||= arguments.shift[/\d+/].to_i
|
625
|
-
@label ||= arguments.shift
|
626
|
-
when "unlabel"
|
627
|
-
@action = :unlabel
|
628
|
-
@number ||= arguments.shift[/\d+/].to_i
|
629
|
-
@label ||= arguments.shift
|
630
|
-
when "comment"
|
631
|
-
@action = :comment
|
632
|
-
@number ||= arguments.shift[/\d+/].to_i
|
633
|
-
when "claim"
|
634
|
-
@action = :claim
|
635
|
-
@number ||= arguments.shift[/\d+/].to_i
|
636
|
-
when %r{^([^/]+)/([^/]+)$}
|
637
|
-
@action = :list
|
638
|
-
@user, @repo = $1, $2
|
639
|
-
when "url", "web"
|
640
|
-
@action = :url
|
641
|
-
@number ||= arguments.shift[/\d+/].to_i
|
642
|
-
end
|
643
|
-
|
644
|
-
@use_ssl ||= `git config github.ssl`.chomp == 'true'
|
645
|
-
|
646
|
-
if @action
|
647
|
-
@args = @argv.dup
|
648
|
-
args.delete_if { |arg| arg == command }
|
649
|
-
option_parser.parse!(*args)
|
650
|
-
return true
|
651
|
-
end
|
652
|
-
unless command.start_with? "-"
|
653
|
-
warn "#{File.basename $0}: what do you mean, '#{command}'?"
|
654
|
-
end
|
655
|
-
end
|
656
|
-
end
|
657
|
-
end
|