ghi 0.9.0.dev1 → 0.9.0.20120627

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