ghi 0.9.0.dev1 → 0.9.0.20120627

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,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'ghi'
3
+ autoload :GHI, 'ghi'
4
4
  GHI.execute ARGV
data/lib/ghi.rb CHANGED
@@ -6,28 +6,29 @@ module GHI
6
6
  autoload :Commands, 'ghi/commands'
7
7
  autoload :Editor, 'ghi/editor'
8
8
  autoload :Formatting, 'ghi/formatting'
9
+ autoload :Web, 'ghi/web'
9
10
 
10
11
  class << self
11
12
  def execute args
12
13
  STDOUT.sync = true
13
14
 
15
+ double_dash = args.index { |arg| arg == '--' }
14
16
  if index = args.index { |arg| arg !~ /^-/ }
15
- command_name = args.delete_at index
16
- command_args = args.slice! index, args.length
17
+ if double_dash.nil? || index < double_dash
18
+ command_name = args.delete_at index
19
+ command_args = args.slice! index, args.length
20
+ end
17
21
  end
18
22
  command_args ||= []
19
23
 
20
24
  option_parser = OptionParser.new do |opts|
21
25
  opts.banner = <<EOF
22
- usage: ghi [--version] [--help] <command> [<args>] [ -- [<user>/]<repo>]
26
+ usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
27
+ [ -- [<user>/]<repo>]
23
28
  EOF
24
- # opts.banner = <<EOF
25
- # usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
26
- # [ -- [<user>/]<repo>]
27
- # EOF
28
29
  opts.on('--version') { command_name = 'version' }
29
30
  opts.on '-p', '--paginate', '--[no-]pager' do |paginate|
30
-
31
+ GHI::Formatting.paginate = paginate
31
32
  end
32
33
  opts.on '--help' do
33
34
  command_args.unshift(*args)
@@ -38,8 +39,17 @@ EOF
38
39
  opts.on '--[no-]color' do |colorize|
39
40
  Formatting::Colors.colorize = colorize
40
41
  end
41
- opts.on('-v') { self.v = true }
42
- opts.on('-h') { raise OptionParser::InvalidOption }
42
+ opts.on '-l' do
43
+ if command_name
44
+ raise OptionParser::InvalidOption
45
+ else
46
+ command_name = 'list'
47
+ end
48
+ end
49
+ opts.on '-v' do
50
+ command_name ? self.v = true : command_name = 'version'
51
+ end
52
+ opts.on('-V') { command_name = 'version' }
43
53
  end
44
54
 
45
55
  begin
@@ -75,40 +85,46 @@ EOF
75
85
  end
76
86
  rescue SocketError => e
77
87
  abort "Couldn't find internet."
78
- rescue Errno::ECONNREFUSED => e
79
- abort "Couldn't connect to GitHub."
80
- rescue Errno::ETIMEDOUT => e
81
- abort 'Timed out looking for GitHub.'
88
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
89
+ abort "Couldn't find GitHub."
82
90
  end
83
91
  end
84
92
  rescue Authorization::Required => e
85
93
  retry if Authorization.authorize!
86
94
  warn e.message
87
95
  if Authorization.token
88
- warn <<EOF
96
+ warn <<EOF.chomp
89
97
 
90
98
  Not authorized for this action with your token. To regenerate a new token:
91
99
  EOF
92
100
  end
93
101
  warn <<EOF
94
102
 
95
- Please run 'ghi config --auth <username>:<password>'
103
+ Please run 'ghi config --auth <username>'
96
104
  EOF
97
105
  exit 1
98
106
  end
99
107
 
108
+ def config key
109
+ var = key.gsub('core', 'git').gsub('.', '_').upcase
110
+ value = ENV[var] || `git config #{key}`.chomp
111
+ value unless value.empty?
112
+ end
113
+
100
114
  attr_accessor :v
101
115
  alias v? v
102
116
 
103
117
  private
104
118
 
105
- ALIASES = {
106
- 'c' => %w(close),
119
+ ALIASES = Hash.new { |_, key|
120
+ [key] if /^\d+$/ === key
121
+ }.update(
107
122
  'claim' => %w(assign),
108
123
  'e' => %w(edit),
109
124
  'l' => %w(list),
110
125
  'L' => %w(label),
111
126
  'm' => %w(comment),
127
+ 'M' => %w(milestone),
112
128
  'o' => %w(open),
113
129
  'reopen' => %w(open),
114
130
  'rm' => %w(close),
@@ -116,10 +132,21 @@ EOF
116
132
  'st' => %w(list),
117
133
  'tag' => %w(label),
118
134
  'unassign' => %w(assign -d)
119
- }
135
+ )
120
136
 
121
137
  def fetch_alias command, args
122
138
  return command unless fetched = ALIASES[command]
139
+
140
+ # If the <command> is an issue number, check the options to see if an
141
+ # edit or show is desired.
142
+ if fetched.first =~ /^\d+$/
143
+ edit_options = Commands::Edit.new([]).options.top.list
144
+ edit_options.reject! { |arg| !arg.is_a?(OptionParser::Switch) }
145
+ edit_options.map! { |arg| [arg.short, arg.long] }
146
+ edit_options.flatten!
147
+ fetched.unshift((edit_options & args).empty? ? 'show' : 'edit')
148
+ end
149
+
123
150
  command = fetched.shift
124
151
  args.unshift(*fetched)
125
152
  command
@@ -1,20 +1,23 @@
1
+ # encoding: utf-8
2
+
1
3
  module GHI
2
4
  module Authorization
3
5
  extend Formatting
4
6
 
5
7
  class Required < RuntimeError
8
+ def message() 'Authorization required.' end
6
9
  end
7
10
 
8
11
  class << self
9
12
  def token
10
13
  return @token if defined? @token
11
- @token = config 'ghi.token'
14
+ @token = GHI.config 'ghi.token'
12
15
  end
13
16
 
14
17
  def authorize! user = username, pass = password, local = true
15
18
  return false unless user && pass
16
19
 
17
- res = throb {
20
+ res = throb(54, "✔\r") {
18
21
  Client.new(user, pass).post(
19
22
  '/authorizations',
20
23
  :scopes => %w(public_repo repo),
@@ -23,12 +26,12 @@ module GHI
23
26
  )
24
27
  }
25
28
  @token = res.body['token']
26
-
29
+
27
30
  run = []
28
31
  unless username
29
- run << "git config#{' --global ' unless local} github.user #{user}"
32
+ run << "git config#{' --global' unless local} github.user #{user}"
30
33
  end
31
- run << "git config#{' --global ' unless local} ghi.token #{token}"
34
+ run << "git config#{' --global' unless local} ghi.token #{token}"
32
35
 
33
36
  system run.join('; ')
34
37
 
@@ -47,24 +50,17 @@ EOF
47
50
  end
48
51
  end
49
52
  rescue Client::Error => e
50
- abort e.message
53
+ abort "#{e.message}#{CURSOR[:column][0]}"
51
54
  end
52
55
 
53
56
  def username
54
57
  return @username if defined? @username
55
- @username = config 'github.user'
58
+ @username = GHI.config 'github.user'
56
59
  end
57
60
 
58
61
  def password
59
62
  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?
63
+ @password = GHI.config 'github.password'
68
64
  end
69
65
  end
70
66
  end
data/lib/ghi/client.rb CHANGED
@@ -106,9 +106,9 @@ module GHI
106
106
  http.use_ssl = true
107
107
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7
108
108
 
109
- GHI.v? and puts "===> #{method.to_s.upcase} #{path} #{req.body}"
109
+ GHI.v? and puts "\r===> #{method.to_s.upcase} #{path} #{req.body}"
110
110
  res = http.start { http.request req }
111
- GHI.v? and puts "<=== #{res.code}: #{res.body}"
111
+ GHI.v? and puts "\r<=== #{res.code}: #{res.body}"
112
112
 
113
113
  case res
114
114
  when Net::HTTPSuccess
@@ -1,6 +1,8 @@
1
1
  module GHI
2
2
  module Commands
3
3
  class Close < Command
4
+ attr_accessor :web
5
+
4
6
  def options
5
7
  OptionParser.new do |opts|
6
8
  opts.banner = <<EOF
@@ -10,6 +12,7 @@ EOF
10
12
  opts.on '-l', '--list', 'list closed issues' do
11
13
  assigns[:command] = List
12
14
  end
15
+ opts.on('-w', '--web') { self.web = true }
13
16
  opts.separator ''
14
17
  opts.separator 'Issue modification options'
15
18
  opts.on '-m', '--message [<text>]', 'close with message' do |text|
@@ -24,7 +27,9 @@ EOF
24
27
  require_repo
25
28
 
26
29
  if list?
27
- List.execute %W(-sc -- #{repo})
30
+ args.unshift(*%W(-sc -- #{repo}))
31
+ args.unshift '-w' if web
32
+ List.execute args
28
33
  else
29
34
  require_issue
30
35
  if assigns.key? :comment
@@ -6,12 +6,16 @@ module GHI
6
6
  class Command
7
7
  include Formatting
8
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?
9
+ class << self
10
+ attr_accessor :detected_repo
11
+
12
+ def execute args
13
+ command = new args
14
+ if i = args.index('--')
15
+ command.repo = args.slice!(i, args.length)[1] # Raise if too many?
16
+ end
17
+ command.execute
13
18
  end
14
- command.execute
15
19
  end
16
20
 
17
21
  attr_reader :args
@@ -33,8 +37,10 @@ module GHI
33
37
 
34
38
  def repo
35
39
  return @repo if defined? @repo
36
- @repo = ENV['GHI_REPO'] || `git config --local ghi.repo`.chomp
37
- @repo = detect_repo if @repo.empty?
40
+ @repo = GHI.config('ghi.repo') || detect_repo
41
+ if @repo && !@repo.include?('/')
42
+ @repo = [Authorization.username, @repo].join '/'
43
+ end
38
44
  @repo
39
45
  end
40
46
  alias extract_repo repo
@@ -57,20 +63,23 @@ module GHI
57
63
  end
58
64
 
59
65
  def detect_repo
60
- remotes = `git config --get-regexp remote\..+\.url`.split "\n"
61
- remotes.reject! { |r| !r.include? 'github.com'}
66
+ remote = remotes.find { |r| r[:remote] == 'upstream' }
67
+ remote ||= remotes.find { |r| r[:remote] == 'origin' }
68
+ remote ||= remotes.find { |r| r[:user] == Authorization.username }
69
+ Command.detected_repo = true and remote[:repo] if remote
70
+ end
62
71
 
63
- remotes.map! { |r|
72
+ def remotes
73
+ return @remotes if defined? @remotes
74
+ @remotes = `git config --get-regexp remote\..+\.url`.split "\n"
75
+ @remotes.reject! { |r| !r.include? 'github.com'}
76
+ @remotes.map! { |r|
64
77
  remote, user, repo = r.scan(
65
78
  %r{remote\.([^\.]+)\.url .*?([^:/]+)/([^/\s]+?)(?:\.git)?$}
66
79
  ).flatten
67
80
  { :remote => remote, :user => user, :repo => "#{user}/#{repo}" }
68
81
  }
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
82
+ @remotes
74
83
  end
75
84
 
76
85
  def issue
@@ -2,6 +2,8 @@ module GHI
2
2
  module Commands
3
3
  class Comment < Command
4
4
  attr_accessor :comment
5
+ attr_accessor :verbose
6
+ attr_accessor :web
5
7
 
6
8
  def options
7
9
  OptionParser.new do |opts|
@@ -12,6 +14,7 @@ EOF
12
14
  opts.on '-l', '--list', 'list comments' do
13
15
  self.action = 'list'
14
16
  end
17
+ opts.on('-w', '--web') { self.web = true }
15
18
  # opts.on '-v', '--verbose', 'list events, too'
16
19
  opts.separator ''
17
20
  opts.separator 'Comment modification options'
@@ -27,6 +30,9 @@ EOF
27
30
  opts.on '--close', 'close associated issue' do
28
31
  self.action = 'close'
29
32
  end
33
+ opts.on '-v', '--verbose' do
34
+ self.verbose = true
35
+ end
30
36
  opts.separator ''
31
37
  end
32
38
  end
@@ -46,7 +52,11 @@ EOF
46
52
  res = throb { api.get res.next_page }
47
53
  end
48
54
  when 'create'
49
- create
55
+ if web
56
+ Web.new(repo).open "issues/#{issue}#issue_comment_form"
57
+ else
58
+ create
59
+ end
50
60
  when 'update', 'destroy'
51
61
  res = index
52
62
  res = throb { api.get res.last_page } if res.last_page
@@ -66,40 +76,54 @@ EOF
66
76
  protected
67
77
 
68
78
  def index
69
- throb { api.get uri }
79
+ throb { api.get uri, :per_page => 100 }
70
80
  end
71
81
 
72
- def create
73
- require_body
82
+ def create message = 'Commented.'
83
+ e = require_body
74
84
  c = throb { api.post uri, assigns }.body
75
- puts format_comment c
76
- puts 'Commented.'
85
+ puts format_comment(c)
86
+ puts message
87
+ e.unlink if e
77
88
  end
78
89
 
79
90
  def update
80
- require_body
81
- c = throb { api.patch uri, assigns }.body
82
- puts format_comment c
83
- puts 'Updated.'
91
+ create 'Comment updated.'
84
92
  end
85
93
 
86
94
  def destroy
87
95
  throb { api.delete uri }
88
- puts 'Deleted.'
96
+ puts 'Comment deleted.'
89
97
  end
90
98
 
91
99
  private
92
100
 
93
101
  def uri
94
- comment ? comment['url'] : "/repos/#{repo}/issues/#{issue}/comments"
102
+ if comment
103
+ comment['url']
104
+ else
105
+ "/repos/#{repo}/issues/#{issue}/comments"
106
+ end
95
107
  end
96
108
 
97
109
  def require_body
110
+ assigns[:body] = args.join ' ' unless args.empty?
98
111
  return if assigns[:body]
99
- message = Editor.gets format_comment_editor(issue, comment)
100
- abort 'No comment.' if message.nil? || message.empty?
101
- abort 'No change.' if comment && message.strip == comment['body'].strip
112
+ if issue && verbose
113
+ i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
114
+ else
115
+ i = {'number'=>issue}
116
+ end
117
+ filename = "GHI_COMMENT_#{issue}"
118
+ filename << "_#{comment['id']}" if comment
119
+ e = Editor.new filename
120
+ message = e.gets format_comment_editor(i, comment)
121
+ e.unlink 'No comment.' if message.nil? || message.empty?
122
+ if comment && message.strip == comment['body'].strip
123
+ e.unlink 'No change.'
124
+ end
102
125
  assigns[:body] = message if message
126
+ e
103
127
  end
104
128
  end
105
129
  end
@@ -10,11 +10,9 @@ EOF
10
10
  opts.on '--local', 'set for local repo only' do
11
11
  assigns[:local] = true
12
12
  end
13
- opts.on '--auth [<username>:<password>]' do |credentials|
13
+ opts.on '--auth [<username>]' do |username|
14
14
  self.action = 'auth'
15
- username, password = credentials.split ':', 2 if credentials
16
- assigns[:username] = username
17
- assigns[:password] = password
15
+ assigns[:username] = username || Authorization.username
18
16
  end
19
17
  opts.separator ''
20
18
  end
@@ -25,11 +23,33 @@ EOF
25
23
  options.parse! args.empty? ? %w(-h) : args
26
24
 
27
25
  if self.action == 'auth'
26
+ assigns[:password] = Authorization.password || get_password
28
27
  Authorization.authorize!(
29
28
  assigns[:username], assigns[:password], assigns[:local]
30
29
  )
31
30
  end
32
31
  end
32
+
33
+ private
34
+
35
+ def get_password
36
+ print "Enter #{assigns[:username]}'s GitHub password (never stored): "
37
+ current_tty = `stty -g`
38
+ system 'stty raw -echo -icanon isig' if $?.success?
39
+ input = ''
40
+ while char = $stdin.getbyte and not (char == 13 or char == 10)
41
+ if char == 127 or char == 8
42
+ input[-1, 1] = '' unless input.empty?
43
+ else
44
+ input << char.chr
45
+ end
46
+ end
47
+ input
48
+ rescue Interrupt
49
+ print '^C'
50
+ ensure
51
+ system "stty #{current_tty}" unless current_tty.empty?
52
+ end
33
53
  end
34
54
  end
35
55
  end