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 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