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 +2 -4
- data/lib/ghi.rb +112 -42
- data/lib/ghi/authorization.rb +71 -0
- data/lib/ghi/client.rb +126 -0
- data/lib/ghi/commands.rb +20 -0
- data/lib/ghi/commands/assign.rb +53 -0
- data/lib/ghi/commands/close.rb +45 -0
- data/lib/ghi/commands/command.rb +114 -0
- data/lib/ghi/commands/comment.rb +104 -0
- data/lib/ghi/commands/config.rb +35 -0
- data/lib/ghi/commands/edit.rb +49 -0
- data/lib/ghi/commands/help.rb +62 -0
- data/lib/ghi/commands/label.rb +153 -0
- data/lib/ghi/commands/list.rb +133 -0
- data/lib/ghi/commands/milestone.rb +150 -0
- data/lib/ghi/commands/open.rb +69 -0
- data/lib/ghi/commands/show.rb +21 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/formatting.rb +301 -0
- data/lib/ghi/formatting/colors.rb +295 -0
- data/lib/ghi/json.rb +1304 -0
- metadata +71 -49
- data/README.rdoc +0 -126
- data/lib/ghi/api.rb +0 -145
- data/lib/ghi/cli.rb +0 -657
- data/lib/ghi/issue.rb +0 -30
- data/spec/ghi/api_spec.rb +0 -218
- data/spec/ghi/cli_spec.rb +0 -267
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -62
| @@ -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
         |