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.
@@ -0,0 +1,45 @@
1
+ module GHI
2
+ module Commands
3
+ class Close < Command
4
+ def options
5
+ OptionParser.new do |opts|
6
+ opts.banner = <<EOF
7
+ usage: ghi close [options] <issueno>
8
+ EOF
9
+ opts.separator ''
10
+ opts.on '-l', '--list', 'list closed issues' do
11
+ assigns[:command] = List
12
+ end
13
+ opts.separator ''
14
+ opts.separator 'Issue modification options'
15
+ opts.on '-m', '--message <text>', 'close with message' do |text|
16
+ assigns[:comment] = text
17
+ end
18
+ opts.separator ''
19
+ end
20
+ end
21
+
22
+ def execute
23
+ options.parse! args
24
+ require_repo
25
+
26
+ if list?
27
+ List.execute %W(-sc -- #{repo})
28
+ else
29
+ require_issue
30
+ Edit.execute %W(-sc #{issue} -- #{repo})
31
+ puts 'Closed.'
32
+ if assigns[:comment]
33
+ Comment.execute %W(#{issue} -m #{assigns[:comment]} -- #{repo})
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def list?
41
+ assigns[:command] == List
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,114 @@
1
+ module GHI
2
+ module Commands
3
+ class MissingArgument < RuntimeError
4
+ end
5
+
6
+ class Command
7
+ include Formatting
8
+
9
+ def self.execute args
10
+ command = new args
11
+ if i = args.index('--')
12
+ command.repo = args.slice!(i, args.length)[1] # Raise if too many?
13
+ end
14
+ command.execute
15
+ end
16
+
17
+ attr_reader :args
18
+ attr_writer :issue
19
+ attr_accessor :action
20
+ attr_accessor :verbose
21
+
22
+ def initialize args
23
+ @args = args.map! { |a| a.dup }
24
+ end
25
+
26
+ def assigns
27
+ @assigns ||= {}
28
+ end
29
+
30
+ def api
31
+ @api ||= Client.new
32
+ end
33
+
34
+ def repo
35
+ return @repo if defined? @repo
36
+ @repo = ENV['GHI_REPO'] || `git config --local ghi.repo`.chomp
37
+ @repo = detect_repo if @repo.empty?
38
+ @repo
39
+ end
40
+ alias extract_repo repo
41
+
42
+ def repo= repo
43
+ @repo = repo.dup
44
+ unless @repo.include? '/'
45
+ @repo.insert 0, "#{Authorization.username}/"
46
+ end
47
+ @repo
48
+ end
49
+
50
+ private
51
+
52
+ def require_repo
53
+ return true if repo
54
+ warn 'Not a GitHub repo.'
55
+ warn ''
56
+ abort options.to_s
57
+ end
58
+
59
+ def detect_repo
60
+ remotes = `git config --get-regexp remote\..+\.url`.split "\n"
61
+ remotes.reject! { |r| !r.include? 'github.com'}
62
+
63
+ remotes.map! { |r|
64
+ remote, user, repo = r.scan(
65
+ %r{remote\.([^\.]+)\.url .*?([^:/]+)/([^/\s]+?)(?:\.git)?$}
66
+ ).flatten
67
+ { :remote => remote, :user => user, :repo => "#{user}/#{repo}" }
68
+ }
69
+
70
+ remote = remotes.find { |r| r[:remote] == 'upstream' }
71
+ remote ||= remotes.find { |r| r[:remote] == 'origin' }
72
+ remote ||= remotes.find { |r| r[:user] == Authorization.username }
73
+ remote[:repo] if remote
74
+ end
75
+
76
+ def issue
77
+ return @issue if defined? @issue
78
+ index = args.index { |arg| /^\d+$/ === arg }
79
+ @issue = (args.delete_at index if index)
80
+ end
81
+ alias extract_issue issue
82
+ alias milestone issue
83
+ alias extract_milestone issue
84
+
85
+ def require_issue
86
+ raise MissingArgument, 'Issue required.' unless issue
87
+ end
88
+
89
+ def require_milestone
90
+ raise MissingArgument, 'Milestone required.' unless milestone
91
+ end
92
+
93
+ # Handles, e.g. `--[no-]milestone [<n>]`.
94
+ def any_or_none_or input
95
+ input ? input : { nil => '*', false => 'none' }[input]
96
+ end
97
+
98
+ def page? message = 'Load more?'
99
+ return unless STDIN.tty?
100
+
101
+ STDOUT.print "#{message} [Yn] "
102
+ begin
103
+ system 'stty raw -echo'
104
+ # Continue on y, j, <ENTER>, <DOWN>...
105
+ exit unless [?y, ?Y, ?j, 13, 27].include? STDIN.getc
106
+ STDOUT.print "\r" + ' ' * columns
107
+ STDOUT.print "\r Loading..."
108
+ ensure
109
+ system 'stty -raw echo'
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,104 @@
1
+ module GHI
2
+ module Commands
3
+ class Comment < Command
4
+ attr_accessor :comment
5
+
6
+ def options
7
+ OptionParser.new do |opts|
8
+ opts.banner = <<EOF
9
+ usage: ghi comment [options] <issueno>
10
+ EOF
11
+ opts.separator ''
12
+ opts.on '-l', '--list', 'list comments' do
13
+ self.action = 'list'
14
+ end
15
+ # opts.on '-v', '--verbose', 'list events, too'
16
+ opts.separator ''
17
+ opts.separator 'Comment modification options'
18
+ opts.on '-m', '--message <text>', 'comment body' do |text|
19
+ assigns[:body] = text
20
+ end
21
+ opts.on '--amend', 'amend previous comment' do
22
+ self.action = 'update'
23
+ end
24
+ opts.on '-D', '--delete', 'delete previous comment' do
25
+ self.action = 'destroy'
26
+ end
27
+ opts.on '--close', 'close associated issue' do
28
+ self.action = 'close'
29
+ end
30
+ opts.separator ''
31
+ end
32
+ end
33
+
34
+ def execute
35
+ require_issue
36
+ require_repo
37
+ self.action ||= 'create'
38
+ options.parse! args
39
+
40
+ case action
41
+ when 'list'
42
+ res = index
43
+ loop do
44
+ puts format_comments(res.body)
45
+ break unless res.next_page
46
+ page?
47
+ res = throb { api.get res.next_page }
48
+ end
49
+ when 'create'
50
+ create
51
+ when 'update', 'destroy'
52
+ res = index
53
+ res = throb { api.get res.last_page } if res.last_page
54
+ self.comment = res.body.reverse.find { |c|
55
+ c['user']['login'] == Authorization.username
56
+ }
57
+ if comment
58
+ send action
59
+ else
60
+ abort 'No recent comment found.'
61
+ end
62
+ when 'close'
63
+ Close.execute %W(-m #{assigns[:body]} #{issue} -- #{repo})
64
+ end
65
+ end
66
+
67
+ protected
68
+
69
+ def index
70
+ throb { api.get uri }
71
+ end
72
+
73
+ def create
74
+ require_body
75
+ throb { api.post uri, assigns }
76
+ puts 'Comment created.'
77
+ end
78
+
79
+ def update
80
+ require_body
81
+ throb { api.patch uri, assigns }
82
+ puts 'Comment updated.'
83
+ end
84
+
85
+ def destroy
86
+ throb { api.delete uri }
87
+ puts 'Comment deleted.'
88
+ end
89
+
90
+ private
91
+
92
+ def uri
93
+ comment ? comment['url'] : "/repos/#{repo}/issues/#{issue}/comments"
94
+ end
95
+
96
+ def require_body
97
+ if assigns[:body].nil?
98
+ warn 'Missing argument: -m'
99
+ abort options.to_s
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,35 @@
1
+ module GHI
2
+ module Commands
3
+ class Config < Command
4
+ def options
5
+ OptionParser.new do |opts|
6
+ opts.banner = <<EOF
7
+ usage: ghi config [options]
8
+ EOF
9
+ opts.separator ''
10
+ opts.on '--local', 'set for local repo only' do
11
+ assigns[:local] = true
12
+ end
13
+ opts.on '--auth [<username>:<password>]' do |credentials|
14
+ self.action = 'auth'
15
+ username, password = credentials.split ':', 2 if credentials
16
+ assigns[:username] = username
17
+ assigns[:password] = password
18
+ end
19
+ opts.separator ''
20
+ end
21
+ end
22
+
23
+ def execute
24
+ global = true
25
+ options.parse! args.empty? ? %w(-h) : args
26
+
27
+ if self.action == 'auth'
28
+ Authorization.authorize!(
29
+ assigns[:username], assigns[:password], assigns[:local]
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ module GHI
2
+ module Commands
3
+ class Edit < Command
4
+ def options
5
+ OptionParser.new do |opts|
6
+ opts.banner = <<EOF
7
+ usage: ghi edit [options] <issueno>
8
+ EOF
9
+ opts.separator ''
10
+ opts.on(
11
+ '-m', '--message <text>', 'change issue description'
12
+ ) do |text|
13
+ assigns[:title], assigns[:body] = text.split(/\n+/, 2)
14
+ end
15
+ opts.on(
16
+ '-u', '--[no-]assign [<user>]', 'assign to specified user'
17
+ ) do |assignee|
18
+ assigns[:assignee] = assignee
19
+ end
20
+ opts.on(
21
+ '-s', '--state <in>', %w(open closed),
22
+ {'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
23
+ ) do |state|
24
+ assigns[:state] = state
25
+ end
26
+ opts.on(
27
+ '-M', '--[no-]milestone [<n>]', Integer, 'associate with milestone'
28
+ ) do |milestone|
29
+ assigns[:milestone] = milestone
30
+ end
31
+ opts.on(
32
+ '-L', '--label <labelname>...', Array, 'associate with label(s)'
33
+ ) do |labels|
34
+ assigns[:labels] = labels
35
+ end
36
+ opts.separator ''
37
+ end
38
+ end
39
+
40
+ def execute
41
+ require_issue
42
+ require_repo
43
+ options.parse! args
44
+ i = throb { api.patch "/repos/#{repo}/issues/#{issue}", assigns }.body
45
+ puts format_issue(i)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,62 @@
1
+ module GHI
2
+ module Commands
3
+ class Help < Command
4
+ def self.execute args, message = nil
5
+ new(args).execute message
6
+ end
7
+
8
+ attr_accessor :command
9
+
10
+ def options
11
+ OptionParser.new do |opts|
12
+ opts.banner = 'usage: ghi help [--all] [--man|--web] <command>'
13
+ opts.separator ''
14
+ opts.on('-a', '--all', 'print all available commands') { all }
15
+ opts.on('-m', '--man', 'show man page') { man }
16
+ opts.on('-w', '--web', 'show manual in web browser') { web }
17
+ opts.separator ''
18
+ end
19
+ end
20
+
21
+ def execute message = nil
22
+ self.command = args.shift if args.first !~ /^-/
23
+
24
+ if command.nil? && args.empty?
25
+ puts message if message
26
+ puts <<EOF
27
+
28
+ The most commonly used ghi commands are:
29
+ list List your issues (or a repository's)
30
+ show Show an issue's details
31
+ open Open (or reopen) an issue
32
+ close Close an issue
33
+ edit Modify an existing issue
34
+ comment Leave a comment on an issue
35
+ label Create, list, modify, or delete labels
36
+ assign Assign an issue to yourself (or someone else)
37
+ milestone Manage project milestones
38
+
39
+ See 'ghi help <command>' for more information on a specific command.
40
+ EOF
41
+ exit
42
+ end
43
+
44
+ options.parse! args.empty? ? %w(-m) : args
45
+ end
46
+
47
+ def all
48
+ raise 'TODO'
49
+ end
50
+
51
+ def man
52
+ GHI.execute [command, '-h']
53
+ # TODO:
54
+ # exec "man #{['ghi', command].compact.join '-'}"
55
+ end
56
+
57
+ def web
58
+ raise 'TODO'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,153 @@
1
+ module GHI
2
+ module Commands
3
+ class Label < Command
4
+ attr_accessor :name
5
+
6
+ #--
7
+ # FIXME: This does too much. Opt for a secondary command, e.g.,
8
+ #
9
+ # ghi label add <labelname>
10
+ # ghi label rm <labelname>
11
+ # ghi label <issueno> <labelname>...
12
+ #++
13
+ def options
14
+ OptionParser.new do |opts|
15
+ opts.banner = <<EOF
16
+ usage: ghi label <labelname> [-c <color>] [-r <newname>]
17
+ or: ghi label -D <labelname>
18
+ or: ghi label <issueno> [-a] [-d] [-f]
19
+ or: ghi label -l [<issueno>]
20
+ EOF
21
+ opts.separator ''
22
+ opts.on '-l', '--list [<issueno>]', 'list label names' do |n|
23
+ self.action = 'index'
24
+ @issue ||= n
25
+ end
26
+ opts.on '-D', '--delete', 'delete label' do
27
+ self.action = 'destroy'
28
+ end
29
+ opts.separator ''
30
+ opts.separator 'Label modification options'
31
+ opts.on(
32
+ '-c', '--color <color>', 'color name or 6-character hex code'
33
+ ) do |color|
34
+ assigns[:color] = to_hex color
35
+ self.action ||= 'create'
36
+ end
37
+ opts.on '-r', '--rename <labelname>', 'new label name' do |name|
38
+ assigns[:name] = name
39
+ self.action = 'update'
40
+ end
41
+ opts.separator ''
42
+ opts.separator 'Issue modification options'
43
+ opts.on '-a', '--add', 'add labels to issue' do
44
+ self.action = issue ? 'add' : 'create'
45
+ end
46
+ opts.on '-d', '--delete', 'remove labels from issue' do
47
+ self.action = issue ? 'remove' : 'destroy'
48
+ end
49
+ opts.on '-f', '--force', 'replace existing labels' do
50
+ self.action = issue ? 'replace' : 'update'
51
+ end
52
+ opts.separator ''
53
+ end
54
+ end
55
+
56
+ def execute
57
+ extract_issue
58
+ require_repo
59
+ options.parse! args.empty? ? %w(-l) : args
60
+
61
+ if issue
62
+ self.action ||= 'add'
63
+ self.name = args.shift.to_s.split ','
64
+ else
65
+ self.action ||= 'create'
66
+ self.name ||= args.shift
67
+ end
68
+
69
+ send action
70
+ end
71
+
72
+ protected
73
+
74
+ def index
75
+ if issue
76
+ uri = "/repos/#{repo}/issues/#{issue}/labels"
77
+ else
78
+ uri = "/repos/#{repo}/labels"
79
+ end
80
+ labels = throb { api.get uri }.body
81
+ if labels.empty?
82
+ puts 'None.'
83
+ else
84
+ puts labels.map { |label|
85
+ name = label['name']
86
+ colorize? ? bg(label['color']) { " #{name} " } : name
87
+ }
88
+ end
89
+ end
90
+
91
+ def create
92
+ label = throb {
93
+ api.post "/repos/#{repo}/labels", assigns.merge(:name => name)
94
+ }.body
95
+ return update if label.nil?
96
+ puts "%s created." % bg(label['color']) { " #{label['name']} "}
97
+ rescue Client::Error => e
98
+ if e.errors.find { |error| error['code'] == 'already_exists' }
99
+ return update
100
+ end
101
+ raise
102
+ end
103
+
104
+ def update
105
+ label = throb {
106
+ api.patch "/repos/#{repo}/labels/#{name}", assigns
107
+ }.body
108
+ puts "%s updated." % bg(label['color']) { " #{label['name']} "}
109
+ end
110
+
111
+ def destroy
112
+ throb { api.delete "/repos/#{repo}/labels/#{name}" }
113
+ puts "[#{name}] deleted."
114
+ end
115
+
116
+ def add
117
+ labels = throb {
118
+ api.post "/repos/#{repo}/issues/#{issue}/labels", name
119
+ }.body
120
+ labels.delete_if { |l| !name.include?(l['name']) }
121
+ puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
122
+ end
123
+
124
+ def remove
125
+ case name.length
126
+ when 0
127
+ throb { api.delete base_uri }
128
+ puts "Labels removed."
129
+ when 1
130
+ labels = throb { api.delete "#{base_uri}/#{name.join}" }.body
131
+ puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
132
+ else
133
+ labels = throb {
134
+ api.get "/repos/#{repo}/issues/#{issue}/labels"
135
+ }.body
136
+ self.name = labels.map { |l| l['name'] } - name
137
+ replace
138
+ end
139
+ end
140
+
141
+ def replace
142
+ labels = throb { api.put base_uri, name }.body
143
+ puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
144
+ end
145
+
146
+ private
147
+
148
+ def base_uri
149
+ "/repos/#{repo}/#{issue ? "issues/#{issue}/labels" : 'labels'}"
150
+ end
151
+ end
152
+ end
153
+ end