ghi 0.3.1 → 0.9.0.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -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