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.
data/bin/ghi CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $: << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
4
- require "ghi/cli"
5
- $stdout.sync = true
6
- GHI::CLI::Executable.new.parse!(ARGV)
3
+ require 'ghi'
4
+ GHI.execute ARGV
data/lib/ghi.rb CHANGED
@@ -1,57 +1,127 @@
1
- require "net/http"
2
- require "yaml"
3
- YAML::ENGINE.yamler = "syck" if YAML.const_defined? :ENGINE
1
+ require 'optparse'
4
2
 
5
3
  module GHI
6
- VERSION = "0.3.1"
4
+ autoload :Authorization, 'ghi/authorization'
5
+ autoload :Client, 'ghi/client'
6
+ autoload :Commands, 'ghi/commands'
7
+ autoload :Formatting, 'ghi/formatting'
7
8
 
8
9
  class << self
9
- def login
10
- return @login if defined? @login
11
- @login = ENV["GITHUB_USER"] || `git config --get github.user`.chomp
12
- if @login.empty?
13
- warn "Please configure your GitHub username."
14
- puts
15
- puts "E.g., git config --global github.user [your username]"
16
- puts "Or set the environment variable GITHUB_USER"
17
- abort
10
+ def execute args
11
+ STDOUT.sync = true
12
+
13
+ if index = args.index { |arg| arg !~ /^-/ }
14
+ command_name = args.delete_at index
15
+ command_args = args.slice! index, args.length
16
+ end
17
+ command_args ||= []
18
+
19
+ option_parser = OptionParser.new do |opts|
20
+ opts.banner = <<EOF
21
+ usage: ghi [--version] [--help] <command> [<args>] [ -- [<user>/]<repo>]
22
+ EOF
23
+ # opts.banner = <<EOF
24
+ # usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
25
+ # [ -- [<user>/]<repo>]
26
+ # EOF
27
+ opts.on('--version') { command_name = 'version' }
28
+ opts.on '-p', '--paginate', '--[no-]pager' do |paginate|
29
+
30
+ end
31
+ opts.on '--help' do
32
+ command_args.unshift(*args)
33
+ command_args.unshift command_name if command_name
34
+ args.clear
35
+ command_name = 'help'
36
+ end
37
+ opts.on '--[no-]color' do |colorize|
38
+ Formatting::Colors.colorize = colorize
39
+ end
40
+ opts.on('-v') { self.v = true }
41
+ opts.on('-h') { raise OptionParser::InvalidOption }
18
42
  end
19
- @login
20
- end
21
43
 
22
- def token
23
- return @token if defined? @token
24
- # env values are frozen
25
- @token = (ENV["GITHUB_TOKEN"] || `git config --get github.token`.chomp).dup
26
- if @token.empty?
27
- warn "Please configure your GitHub token."
28
- puts
29
- puts "E.g., git config --global github.token [your token]"
30
- puts "Or set the environment variable GITHUB_TOKEN"
31
- puts
32
- puts "Find your token here: https://github.com/account/admin"
33
- abort
34
- elsif @token.sub!(/^!/, '')
35
- @token = `#@token`
44
+ begin
45
+ option_parser.parse! args
46
+ rescue OptionParser::InvalidOption => e
47
+ warn e.message.capitalize
48
+ abort option_parser.banner
36
49
  end
37
- @token
50
+
51
+ if command_name.nil? || command_name == 'help'
52
+ Commands::Help.execute command_args, option_parser.banner
53
+ else
54
+ command_name = fetch_alias command_name, command_args
55
+ begin
56
+ command = Commands.const_get command_name.capitalize
57
+ rescue NameError
58
+ abort "ghi: '#{command_name}' is not a ghi command. See 'ghi --help'."
59
+ end
60
+
61
+ # Post-command help option parsing.
62
+ Commands::Help.execute [command_name] if command_args.first == '--help'
63
+
64
+ begin
65
+ command.execute command_args
66
+ rescue OptionParser::ParseError, Commands::MissingArgument => e
67
+ warn "#{e.message.capitalize}\n"
68
+ abort command.new([]).options.to_s
69
+ rescue Client::Error => e
70
+ if e.response.is_a?(Net::HTTPNotFound) && Authorization.token.nil?
71
+ raise Authorization::Required
72
+ else
73
+ abort e.message
74
+ end
75
+ rescue SocketError => e
76
+ abort "Couldn't find internet."
77
+ rescue Errno::ECONNREFUSED => e
78
+ abort "Couldn't connect to GitHub."
79
+ rescue Errno::ETIMEDOUT => e
80
+ abort 'Timed out looking for GitHub.'
81
+ end
82
+ end
83
+ rescue Authorization::Required => e
84
+ retry if Authorization.authorize!
85
+ warn e.message
86
+ if Authorization.token
87
+ warn <<EOF
88
+
89
+ Not authorized for this action with your token. To regenerate a new token:
90
+ EOF
91
+ end
92
+ warn <<EOF
93
+
94
+ Please run 'ghi config --auth <username>:<password>'
95
+ EOF
96
+ exit 1
38
97
  end
39
98
 
99
+ attr_accessor :v
100
+ alias v? v
101
+
40
102
  private
41
103
 
42
- def user?(username)
43
- url = "http://github.com/api/v2/yaml/user/show/#{username}"
44
- !YAML.load(Net::HTTP.get(URI.parse(url)))["user"].nil?
45
- rescue ArgumentError, URI::InvalidURIError
46
- false
47
- end
104
+ ALIASES = {
105
+ 'c' => %w(close),
106
+ 'claim' => %w(assign),
107
+ 'e' => %w(edit),
108
+ 'l' => %w(list),
109
+ 'L' => %w(label),
110
+ 'm' => %w(comment),
111
+ 'o' => %w(open),
112
+ 'reopen' => %w(open),
113
+ 'rm' => %w(close),
114
+ 's' => %w(show),
115
+ 'st' => %w(list),
116
+ 'tag' => %w(label),
117
+ 'unassign' => %w(assign -d)
118
+ }
48
119
 
49
- def token?(token)
50
- url = "http://github.com/api/v2/yaml/user/show/#{login}"
51
- url += "?login=#{login}&token=#{token}"
52
- !YAML.load(Net::HTTP.get(URI.parse(url)))["user"]["plan"].nil?
53
- rescue ArgumentError, NoMethodError, URI::InvalidURIError
54
- false
120
+ def fetch_alias command, args
121
+ return command unless fetched = ALIASES[command]
122
+ command = fetched.shift
123
+ args.unshift(*fetched)
124
+ command
55
125
  end
56
126
  end
57
127
  end
@@ -0,0 +1,71 @@
1
+ module GHI
2
+ module Authorization
3
+ extend Formatting
4
+
5
+ class Required < RuntimeError
6
+ end
7
+
8
+ class << self
9
+ def token
10
+ return @token if defined? @token
11
+ @token = config 'ghi.token'
12
+ end
13
+
14
+ def authorize! user = username, pass = password, local = true
15
+ return false unless user && pass
16
+
17
+ res = throb {
18
+ Client.new(user, pass).post(
19
+ '/authorizations',
20
+ :scopes => %w(public_repo repo),
21
+ :note => 'ghi',
22
+ :note_url => 'https://github.com/stephencelis/ghi'
23
+ )
24
+ }
25
+ @token = res['token']
26
+
27
+ run = []
28
+ unless username
29
+ run << "git config#{' --global ' unless local} github.user #{user}"
30
+ end
31
+ run << "git config#{' --global ' unless local} ghi.token #{token}"
32
+
33
+ system run.join('; ')
34
+
35
+ unless local
36
+ at_exit do
37
+ warn <<EOF
38
+ Your ~/.gitconfig has been modified by way of:
39
+
40
+ #{run.join "\n "}
41
+
42
+ #{bright { blink { 'Do not check this change into public source control!' } }}
43
+ Alternatively, set the following env var in a private dotfile:
44
+
45
+ export GHI_TOKEN="#{token}"
46
+ EOF
47
+ end
48
+ end
49
+ rescue Client::Error => e
50
+ abort e.message
51
+ end
52
+
53
+ def username
54
+ return @username if defined? @username
55
+ @username = config 'github.user'
56
+ end
57
+
58
+ def password
59
+ return @password if defined? @password
60
+ @password = config 'github.password'
61
+ end
62
+
63
+ private
64
+
65
+ def config key
66
+ value = ENV["#{key.upcase.gsub '.', '_'}"] || `git config #{key}`.chomp
67
+ value unless value.empty?
68
+ end
69
+ end
70
+ end
71
+ end
data/lib/ghi/client.rb ADDED
@@ -0,0 +1,126 @@
1
+ require 'cgi'
2
+ require 'net/https'
3
+
4
+ unless defined? Net::HTTP::Patch
5
+ Net::HTTP::Patch = Class.new Net::HTTP::Post do
6
+ METHOD = 'PATCH'
7
+ end
8
+ end
9
+
10
+ module GHI
11
+ class Client
12
+ autoload :JSON, 'ghi/json'
13
+
14
+ class Error < RuntimeError
15
+ attr_reader :response
16
+ def initialize response
17
+ @response, @json = response, JSON.parse(response.body)
18
+ end
19
+
20
+ def body() @json end
21
+ def message() body['message'] end
22
+ def errors() [*body['errors']] end
23
+ end
24
+
25
+ class Response
26
+ def initialize response
27
+ @response = response
28
+ end
29
+
30
+ def body
31
+ @body ||= JSON.parse @response.body
32
+ end
33
+
34
+ def next_page() links['next'] end
35
+ def last_page() links['last'] end
36
+
37
+ private
38
+
39
+ def links
40
+ return @links if defined? @links
41
+ @links = {}
42
+ if links = @response['Link']
43
+ links.scan(/<([^>]+)>; rel="([^"]+)"/).each { |l, r| @links[r] = l }
44
+ end
45
+ @links
46
+ end
47
+ end
48
+
49
+ CONTENT_TYPE = 'application/vnd.github+json'
50
+ METHODS = {
51
+ :head => Net::HTTP::Head,
52
+ :get => Net::HTTP::Get,
53
+ :post => Net::HTTP::Post,
54
+ :put => Net::HTTP::Put,
55
+ :patch => Net::HTTP::Patch,
56
+ :delete => Net::HTTP::Delete
57
+ }
58
+
59
+ attr_reader :username, :password
60
+ def initialize username = nil, password = nil
61
+ @username, @password = username, password
62
+ end
63
+
64
+ def head path, options = {}
65
+ request :head, path, options
66
+ end
67
+
68
+ def get path, params = {}, options = {}
69
+ request :get, path, options.merge(:params => params)
70
+ end
71
+
72
+ def post path, body = nil, options = {}
73
+ request :post, path, options.merge(:body => body)
74
+ end
75
+
76
+ def put path, body = nil, options = {}
77
+ request :put, path, options.merge(:body => body)
78
+ end
79
+
80
+ def patch path, body = nil, options = {}
81
+ request :patch, path, options.merge(:body => body)
82
+ end
83
+
84
+ def delete path, options = {}
85
+ request :delete, path, options
86
+ end
87
+
88
+ private
89
+
90
+ def request method, path, options
91
+ if params = options[:params] and !params.empty?
92
+ q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" }
93
+ path += "?#{q.join '&'}"
94
+ end
95
+
96
+ req = METHODS[method].new path, 'Accept' => CONTENT_TYPE
97
+ if GHI::Authorization.token
98
+ req['Authorization'] = "token #{GHI::Authorization.token}"
99
+ end
100
+ if options.key? :body
101
+ req['Content-Type'] = CONTENT_TYPE
102
+ req.body = options[:body] ? JSON.dump(options[:body]) : ''
103
+ end
104
+ req.basic_auth username, password if username && password
105
+
106
+ http = Net::HTTP.new 'api.github.com', 443
107
+ http.use_ssl = true
108
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7
109
+
110
+ GHI.v? and puts "===> #{method.to_s.upcase} #{path} #{req.body}"
111
+ res = http.start { http.request req }
112
+ GHI.v? and puts "<=== #{res.code}: #{res.body}"
113
+
114
+ case res
115
+ when Net::HTTPSuccess
116
+ return Response.new(res)
117
+ when Net::HTTPUnauthorized
118
+ if password.nil?
119
+ raise Authorization::Required, 'Authorization required'
120
+ end
121
+ end
122
+
123
+ raise Error, res
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,20 @@
1
+ module GHI
2
+ module Commands
3
+ autoload :Command, 'ghi/commands/command'
4
+
5
+ autoload :List, 'ghi/commands/list'
6
+ autoload :Open, 'ghi/commands/open'
7
+ autoload :Assign, 'ghi/commands/assign'
8
+ autoload :Close, 'ghi/commands/close'
9
+ autoload :Comment, 'ghi/commands/comment'
10
+ autoload :Config, 'ghi/commands/config'
11
+ autoload :Edit, 'ghi/commands/edit'
12
+ autoload :Help, 'ghi/commands/help'
13
+ autoload :Label, 'ghi/commands/label'
14
+ autoload :Milestone, 'ghi/commands/milestone'
15
+ autoload :Reopen, 'ghi/commands/reopen'
16
+ autoload :Show, 'ghi/commands/show'
17
+ autoload :Unassign, 'ghi/commands/unassign'
18
+ autoload :Version, 'ghi/commands/version'
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ module GHI
2
+ module Commands
3
+ class Assign < Command
4
+ def options
5
+ OptionParser.new do |opts|
6
+ opts.banner = <<EOF
7
+ usage: ghi assign [options] [<issueno>]
8
+ or: ghi assign <issueno> <user>
9
+ or: ghi unassign <issueno>
10
+ EOF
11
+ opts.separator ''
12
+ opts.on(
13
+ '-u', '--assignee <user>', 'assign to specified user'
14
+ ) do |assignee|
15
+ assigns[:assignee] = assignee
16
+ end
17
+ opts.on '-d', '--no-assignee', 'unassign this issue' do
18
+ assigns[:assignee] = nil
19
+ end
20
+ opts.on '-l', '--list', 'list assigned issues' do
21
+ self.action = 'list'
22
+ end
23
+ opts.separator ''
24
+ end
25
+ end
26
+
27
+ def execute
28
+ self.action = 'edit'
29
+ assigns[:args] = []
30
+
31
+ require_repo
32
+ extract_issue
33
+ options.parse! args
34
+
35
+ unless assigns.key? :assignee
36
+ assigns[:assignee] = args.pop || Authorization.username
37
+ end
38
+ if assigns.key? :assignee
39
+ assigns[:args].concat(
40
+ assigns[:assignee] ? %W(-u #{assigns[:assignee]}) : %w(--no-assign)
41
+ )
42
+ end
43
+ assigns[:args] << issue if issue
44
+ assigns[:args].concat %W(-- #{repo})
45
+
46
+ case action
47
+ when 'list' then List.execute assigns[:args]
48
+ when 'edit' then Edit.execute assigns[:args]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end