diff --git a/Rakefile b/Rakefile index 7726d8c..ee288b9 100644 --- a/Rakefile +++ b/Rakefile @@ -172,7 +172,7 @@ def install_homebrew puts "======================================================" puts "Installing Homebrew packages...There may be some warnings." puts "======================================================" - run %{brew install zsh ctags git hub tmux reattach-to-user-namespace the_silver_searcher} + run %{brew install zsh ctags git hub tmux reattach-to-user-namespace the_silver_searcher ghi hub} run %{brew install macvim --custom-icons --override-system-vim --with-lua --with-luajit} puts puts diff --git a/bin/ghi b/bin/ghi deleted file mode 100755 index 6fc885a..0000000 --- a/bin/ghi +++ /dev/null @@ -1,3738 +0,0 @@ -#!/usr/bin/env ruby -# encoding: utf-8 -require 'optparse' - -module GHI - class << self - def execute args - STDOUT.sync = true - - double_dash = args.index { |arg| arg == '--' } - if index = args.index { |arg| arg !~ /^-/ } - if double_dash.nil? || index < double_dash - command_name = args.delete_at index - command_args = args.slice! index, args.length - end - end - command_args ||= [] - - option_parser = OptionParser.new do |opts| - opts.banner = < [] - [ -- [/]] -EOF - opts.on('--version') { command_name = 'version' } - opts.on '-p', '--paginate', '--[no-]pager' do |paginate| - GHI::Formatting.paginate = paginate - end - opts.on '--help' do - command_args.unshift(*args) - command_args.unshift command_name if command_name - args.clear - command_name = 'help' - end - opts.on '--[no-]color' do |colorize| - Formatting::Colors.colorize = colorize - end - opts.on '-l' do - if command_name - raise OptionParser::InvalidOption - else - command_name = 'list' - end - end - opts.on '-v' do - command_name ? self.v = true : command_name = 'version' - end - opts.on('-V') { command_name = 'version' } - end - - begin - option_parser.parse! args - rescue OptionParser::InvalidOption => e - warn e.message.capitalize - abort option_parser.banner - end - - if command_name.nil? || command_name == 'help' - Commands::Help.execute command_args, option_parser.banner - else - command_name = fetch_alias command_name, command_args - begin - command = Commands.const_get command_name.capitalize - rescue NameError - abort "ghi: '#{command_name}' is not a ghi command. See 'ghi --help'." - end - - # Post-command help option parsing. - Commands::Help.execute [command_name] if command_args.first == '--help' - - begin - command.execute command_args - rescue OptionParser::ParseError, Commands::MissingArgument => e - warn "#{e.message.capitalize}\n" - abort command.new([]).options.to_s - rescue Client::Error => e - if e.response.is_a?(Net::HTTPNotFound) && Authorization.token.nil? - raise Authorization::Required - else - abort e.message - end - rescue SocketError => e - abort "Couldn't find internet." - rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e - abort "Couldn't find GitHub." - end - end - rescue Authorization::Required => e - retry if Authorization.authorize! - warn e.message - if Authorization.token - warn <' -EOF - exit 1 - end - - def config key - var = key.gsub('core', 'git').gsub('.', '_').upcase - value = ENV[var] || `git config #{key}`.chomp - value unless value.empty? - end - - attr_accessor :v - alias v? v - - private - - ALIASES = Hash.new { |_, key| - [key] if /^\d+$/ === key - }.update( - 'claim' => %w(assign), - 'create' => %w(open), - 'e' => %w(edit), - 'l' => %w(list), - 'L' => %w(label), - 'm' => %w(comment), - 'M' => %w(milestone), - 'new' => %w(open), - 'o' => %w(open), - 'reopen' => %w(open), - 'rm' => %w(close), - 's' => %w(show), - 'st' => %w(list), - 'tag' => %w(label), - 'unassign' => %w(assign -d), - 'update' => %w(edit) - ) - - def fetch_alias command, args - return command unless fetched = ALIASES[command] - - # If the is an issue number, check the options to see if an - # edit or show is desired. - if fetched.first =~ /^\d+$/ - edit_options = Commands::Edit.new([]).options.top.list - edit_options.reject! { |arg| !arg.is_a?(OptionParser::Switch) } - edit_options.map! { |arg| [arg.short, arg.long] } - edit_options.flatten! - fetched.unshift((edit_options & args).empty? ? 'show' : 'edit') - end - - command = fetched.shift - args.unshift(*fetched) - command - end - end -end -require 'strscan' - -module JSON - module Pure - # This class implements the JSON parser that is used to parse a JSON string - # into a Ruby data structure. - class Parser < StringScanner - STRING = /" ((?:[^\x0-\x1f"\\] | - # escaped special characters: - \\["\\\/bfnrt] | - \\u[0-9a-fA-F]{4} | - # match all but escaped special characters: - \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*) - "/nx - INTEGER = /(-?0|-?[1-9]\d*)/ - FLOAT = /(-? - (?:0|[1-9]\d*) - (?: - \.\d+(?i:e[+-]?\d+) | - \.\d+ | - (?i:e[+-]?\d+) - ) - )/x - NAN = /NaN/ - INFINITY = /Infinity/ - MINUS_INFINITY = /-Infinity/ - OBJECT_OPEN = /\{/ - OBJECT_CLOSE = /\}/ - ARRAY_OPEN = /\[/ - ARRAY_CLOSE = /\]/ - PAIR_DELIMITER = /:/ - COLLECTION_DELIMITER = /,/ - TRUE = /true/ - FALSE = /false/ - NULL = /null/ - IGNORE = %r( - (?: - //[^\n\r]*[\n\r]| # line comments - /\* # c-style comments - (?: - [^*/]| # normal chars - /[^*]| # slashes that do not start a nested comment - \*[^/]| # asterisks that do not end this comment - /(?=\*/) # single slash before this comment's end - )* - \*/ # the End of this comment - |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr - )+ - )mx - - UNPARSED = Object.new - - # Creates a new JSON::Pure::Parser instance for the string _source_. - # - # It will be configured by the _opts_ hash. _opts_ can have the following - # keys: - # * *max_nesting*: The maximum depth of nesting allowed in the parsed data - # structures. Disable depth checking with :max_nesting => false|nil|0, - # it defaults to 19. - # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in - # defiance of RFC 4627 to be parsed by the Parser. This option defaults - # to false. - # * *symbolize_names*: If set to true, returns symbols for the names - # (keys) in a JSON object. Otherwise strings are returned, which is also - # the default. - # * *create_additions*: If set to false, the Parser doesn't create - # additions even if a matchin class and create_id was found. This option - # defaults to true. - # * *object_class*: Defaults to Hash - # * *array_class*: Defaults to Array - # * *quirks_mode*: Enables quirks_mode for parser, that is for example - # parsing single JSON values instead of documents is possible. - def initialize(source, opts = {}) - opts ||= {} - unless @quirks_mode = opts[:quirks_mode] - source = convert_encoding source - end - super source - if !opts.key?(:max_nesting) # defaults to 19 - @max_nesting = 19 - elsif opts[:max_nesting] - @max_nesting = opts[:max_nesting] - else - @max_nesting = 0 - end - @allow_nan = !!opts[:allow_nan] - @symbolize_names = !!opts[:symbolize_names] - if opts.key?(:create_additions) - @create_additions = !!opts[:create_additions] - else - @create_additions = true - end - @create_id = @create_additions ? JSON.create_id : nil - @object_class = opts[:object_class] || Hash - @array_class = opts[:array_class] || Array - @match_string = opts[:match_string] - end - - alias source string - - def quirks_mode? - !!@quirks_mode - end - - def reset - super - @current_nesting = 0 - end - - # Parses the current JSON string _source_ and returns the complete data - # structure as a result. - def parse - reset - obj = nil - if @quirks_mode - while !eos? && skip(IGNORE) - end - if eos? - raise ParserError, "source did not contain any JSON!" - else - obj = parse_value - obj == UNPARSED and raise ParserError, "source did not contain any JSON!" - end - else - until eos? - case - when scan(OBJECT_OPEN) - obj and raise ParserError, "source '#{peek(20)}' not in JSON!" - @current_nesting = 1 - obj = parse_object - when scan(ARRAY_OPEN) - obj and raise ParserError, "source '#{peek(20)}' not in JSON!" - @current_nesting = 1 - obj = parse_array - when skip(IGNORE) - ; - else - raise ParserError, "source '#{peek(20)}' not in JSON!" - end - end - obj or raise ParserError, "source did not contain any JSON!" - end - obj - end - - private - - def convert_encoding(source) - if source.respond_to?(:to_str) - source = source.to_str - else - raise TypeError, "#{source.inspect} is not like a string" - end - if defined?(::Encoding) - if source.encoding == ::Encoding::ASCII_8BIT - b = source[0, 4].bytes.to_a - source = - case - when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 - source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8) - when b.size >= 4 && b[0] == 0 && b[2] == 0 - source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8) - when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 - source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8) - when b.size >= 4 && b[1] == 0 && b[3] == 0 - source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8) - else - source.dup - end - else - source = source.encode(::Encoding::UTF_8) - end - source.force_encoding(::Encoding::ASCII_8BIT) - else - b = source - source = - case - when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 - JSON.iconv('utf-8', 'utf-32be', b) - when b.size >= 4 && b[0] == 0 && b[2] == 0 - JSON.iconv('utf-8', 'utf-16be', b) - when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 - JSON.iconv('utf-8', 'utf-32le', b) - when b.size >= 4 && b[1] == 0 && b[3] == 0 - JSON.iconv('utf-8', 'utf-16le', b) - else - b - end - end - source - end - - # Unescape characters in strings. - UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } - UNESCAPE_MAP.update({ - ?" => '"', - ?\\ => '\\', - ?/ => '/', - ?b => "\b", - ?f => "\f", - ?n => "\n", - ?r => "\r", - ?t => "\t", - ?u => nil, - }) - - EMPTY_8BIT_STRING = '' - if ::String.method_defined?(:encode) - EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT - end - - def parse_string - if scan(STRING) - return '' if self[1].empty? - string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c| - if u = UNESCAPE_MAP[$&[1]] - u - else # \uXXXX - bytes = EMPTY_8BIT_STRING.dup - i = 0 - while c[6 * i] == ?\\ && c[6 * i + 1] == ?u - bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) - i += 1 - end - JSON.iconv('utf-8', 'utf-16be', bytes) - end - end - if string.respond_to?(:force_encoding) - string.force_encoding(::Encoding::UTF_8) - end - if @create_additions and @match_string - for (regexp, klass) in @match_string - klass.json_creatable? or next - string =~ regexp and return klass.json_create(string) - end - end - string - else - UNPARSED - end - rescue => e - raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}" - end - - def parse_value - case - when scan(FLOAT) - Float(self[1]) - when scan(INTEGER) - Integer(self[1]) - when scan(TRUE) - true - when scan(FALSE) - false - when scan(NULL) - nil - when (string = parse_string) != UNPARSED - string - when scan(ARRAY_OPEN) - @current_nesting += 1 - ary = parse_array - @current_nesting -= 1 - ary - when scan(OBJECT_OPEN) - @current_nesting += 1 - obj = parse_object - @current_nesting -= 1 - obj - when @allow_nan && scan(NAN) - NaN - when @allow_nan && scan(INFINITY) - Infinity - when @allow_nan && scan(MINUS_INFINITY) - MinusInfinity - else - UNPARSED - end - end - - def parse_array - raise NestingError, "nesting of #@current_nesting is too deep" if - @max_nesting.nonzero? && @current_nesting > @max_nesting - result = @array_class.new - delim = false - until eos? - case - when (value = parse_value) != UNPARSED - delim = false - result << value - skip(IGNORE) - if scan(COLLECTION_DELIMITER) - delim = true - elsif match?(ARRAY_CLOSE) - ; - else - raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!" - end - when scan(ARRAY_CLOSE) - if delim - raise ParserError, "expected next element in array at '#{peek(20)}'!" - end - break - when skip(IGNORE) - ; - else - raise ParserError, "unexpected token in array at '#{peek(20)}'!" - end - end - result - end - - def parse_object - raise NestingError, "nesting of #@current_nesting is too deep" if - @max_nesting.nonzero? && @current_nesting > @max_nesting - result = @object_class.new - delim = false - until eos? - case - when (string = parse_string) != UNPARSED - skip(IGNORE) - unless scan(PAIR_DELIMITER) - raise ParserError, "expected ':' in object at '#{peek(20)}'!" - end - skip(IGNORE) - unless (value = parse_value).equal? UNPARSED - result[@symbolize_names ? string.to_sym : string] = value - delim = false - skip(IGNORE) - if scan(COLLECTION_DELIMITER) - delim = true - elsif match?(OBJECT_CLOSE) - ; - else - raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!" - end - else - raise ParserError, "expected value in object at '#{peek(20)}'!" - end - when scan(OBJECT_CLOSE) - if delim - raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" - end - if @create_additions and klassname = result[@create_id] - klass = JSON.deep_const_get klassname - break unless klass and klass.json_creatable? - result = klass.json_create(result) - end - break - when skip(IGNORE) - ; - else - raise ParserError, "unexpected token in object at '#{peek(20)}'!" - end - end - result - end - end - end -end - -module JSON - MAP = { - "\x0" => '\u0000', - "\x1" => '\u0001', - "\x2" => '\u0002', - "\x3" => '\u0003', - "\x4" => '\u0004', - "\x5" => '\u0005', - "\x6" => '\u0006', - "\x7" => '\u0007', - "\b" => '\b', - "\t" => '\t', - "\n" => '\n', - "\xb" => '\u000b', - "\f" => '\f', - "\r" => '\r', - "\xe" => '\u000e', - "\xf" => '\u000f', - "\x10" => '\u0010', - "\x11" => '\u0011', - "\x12" => '\u0012', - "\x13" => '\u0013', - "\x14" => '\u0014', - "\x15" => '\u0015', - "\x16" => '\u0016', - "\x17" => '\u0017', - "\x18" => '\u0018', - "\x19" => '\u0019', - "\x1a" => '\u001a', - "\x1b" => '\u001b', - "\x1c" => '\u001c', - "\x1d" => '\u001d', - "\x1e" => '\u001e', - "\x1f" => '\u001f', - '"' => '\"', - '\\' => '\\\\', - } # :nodoc: - - # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with - # UTF16 big endian characters as \u????, and return it. - if defined?(::Encoding) - def utf8_to_json(string) # :nodoc: - string = string.dup - string << '' # XXX workaround: avoid buffer sharing - string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } - string.force_encoding(::Encoding::UTF_8) - string - end - - def utf8_to_json_ascii(string) # :nodoc: - string = string.dup - string << '' # XXX workaround: avoid buffer sharing - string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } - string.gsub!(/( - (?: - [\xc2-\xdf][\x80-\xbf] | - [\xe0-\xef][\x80-\xbf]{2} | - [\xf0-\xf4][\x80-\xbf]{3} - )+ | - [\x80-\xc1\xf5-\xff] # invalid - )/nx) { |c| - c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" - s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0] - s.gsub!(/.{4}/n, '\\\\u\&') - } - string.force_encoding(::Encoding::UTF_8) - string - rescue => e - raise GeneratorError, "Caught #{e.class}: #{e}" - end - else - def utf8_to_json(string) # :nodoc: - string.gsub(/["\\\x0-\x1f]/) { MAP[$&] } - end - - def utf8_to_json_ascii(string) # :nodoc: - string = string.gsub(/["\\\x0-\x1f]/) { MAP[$&] } - string.gsub!(/( - (?: - [\xc2-\xdf][\x80-\xbf] | - [\xe0-\xef][\x80-\xbf]{2} | - [\xf0-\xf4][\x80-\xbf]{3} - )+ | - [\x80-\xc1\xf5-\xff] # invalid - )/nx) { |c| - c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" - s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0] - s.gsub!(/.{4}/n, '\\\\u\&') - } - string - rescue => e - raise GeneratorError, "Caught #{e.class}: #{e}" - end - end - module_function :utf8_to_json, :utf8_to_json_ascii - - module Pure - module Generator - # This class is used to create State instances, that are use to hold data - # while generating a JSON text from a Ruby data structure. - class State - # Creates a State object from _opts_, which ought to be Hash to create - # a new State instance configured by _opts_, something else to create - # an unconfigured instance. If _opts_ is a State object, it is just - # returned. - def self.from_state(opts) - case - when self === opts - opts - when opts.respond_to?(:to_hash) - new(opts.to_hash) - when opts.respond_to?(:to_h) - new(opts.to_h) - else - SAFE_STATE_PROTOTYPE.dup - end - end - - # Instantiates a new State object, configured by _opts_. - # - # _opts_ can have the following keys: - # - # * *indent*: a string used to indent levels (default: ''), - # * *space*: a string that is put after, a : or , delimiter (default: ''), - # * *space_before*: a string that is put before a : pair delimiter (default: ''), - # * *object_nl*: a string that is put at the end of a JSON object (default: ''), - # * *array_nl*: a string that is put at the end of a JSON array (default: ''), - # * *check_circular*: is deprecated now, use the :max_nesting option instead, - # * *max_nesting*: sets the maximum level of data structure nesting in - # the generated JSON, max_nesting = 0 if no maximum should be checked. - # * *allow_nan*: true if NaN, Infinity, and -Infinity should be - # generated, otherwise an exception is thrown, if these values are - # encountered. This options defaults to false. - # * *quirks_mode*: Enables quirks_mode for parser, that is for example - # generating single JSON values instead of documents is possible. - def initialize(opts = {}) - @indent = '' - @space = '' - @space_before = '' - @object_nl = '' - @array_nl = '' - @allow_nan = false - @ascii_only = false - @quirks_mode = false - @buffer_initial_length = 1024 - configure opts - end - - # This string is used to indent levels in the JSON text. - attr_accessor :indent - - # This string is used to insert a space between the tokens in a JSON - # string. - attr_accessor :space - - # This string is used to insert a space before the ':' in JSON objects. - attr_accessor :space_before - - # This string is put at the end of a line that holds a JSON object (or - # Hash). - attr_accessor :object_nl - - # This string is put at the end of a line that holds a JSON array. - attr_accessor :array_nl - - # This integer returns the maximum level of data structure nesting in - # the generated JSON, max_nesting = 0 if no maximum is checked. - attr_accessor :max_nesting - - # If this attribute is set to true, quirks mode is enabled, otherwise - # it's disabled. - attr_accessor :quirks_mode - - # :stopdoc: - attr_reader :buffer_initial_length - - def buffer_initial_length=(length) - if length > 0 - @buffer_initial_length = length - end - end - # :startdoc: - - # This integer returns the current depth data structure nesting in the - # generated JSON. - attr_accessor :depth - - def check_max_nesting # :nodoc: - return if @max_nesting.zero? - current_nesting = depth + 1 - current_nesting > @max_nesting and - raise NestingError, "nesting of #{current_nesting} is too deep" - end - - # Returns true, if circular data structures are checked, - # otherwise returns false. - def check_circular? - !@max_nesting.zero? - end - - # Returns true if NaN, Infinity, and -Infinity should be considered as - # valid JSON and output. - def allow_nan? - @allow_nan - end - - # Returns true, if only ASCII characters should be generated. Otherwise - # returns false. - def ascii_only? - @ascii_only - end - - # Returns true, if quirks mode is enabled. Otherwise returns false. - def quirks_mode? - @quirks_mode - end - - # Configure this State instance with the Hash _opts_, and return - # itself. - def configure(opts) - @indent = opts[:indent] if opts.key?(:indent) - @space = opts[:space] if opts.key?(:space) - @space_before = opts[:space_before] if opts.key?(:space_before) - @object_nl = opts[:object_nl] if opts.key?(:object_nl) - @array_nl = opts[:array_nl] if opts.key?(:array_nl) - @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) - @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) - @depth = opts[:depth] || 0 - @quirks_mode = opts[:quirks_mode] if opts.key?(:quirks_mode) - if !opts.key?(:max_nesting) # defaults to 19 - @max_nesting = 19 - elsif opts[:max_nesting] - @max_nesting = opts[:max_nesting] - else - @max_nesting = 0 - end - self - end - alias merge configure - - # Returns the configuration instance variables as a hash, that can be - # passed to the configure method. - def to_h - result = {} - for iv in %w[indent space space_before object_nl array_nl allow_nan max_nesting ascii_only quirks_mode buffer_initial_length depth] - result[iv.intern] = instance_variable_get("@#{iv}") - end - result - end - - # Generates a valid JSON document from object +obj+ and returns the - # result. If no valid JSON document can be created this method raises a - # GeneratorError exception. - def generate(obj) - result = obj.to_json(self) - unless @quirks_mode - unless result =~ /\A\s*\[/ && result =~ /\]\s*\Z/ || - result =~ /\A\s*\{/ && result =~ /\}\s*\Z/ - then - raise GeneratorError, "only generation of JSON objects or arrays allowed" - end - end - result - end - - # Return the value returned by method +name+. - def [](name) - __send__ name - end - end - - module GeneratorMethods - module Object - # Converts this object to a string (calling #to_s), converts - # it to a JSON string, and returns the result. This is a fallback, if no - # special method #to_json was defined for some object. - def to_json(*) to_s.to_json end - end - - module Hash - # Returns a JSON string containing a JSON object, that is unparsed from - # this Hash instance. - # _state_ is a JSON::State object, that can also be used to configure the - # produced JSON string output further. - # _depth_ is used to find out nesting depth, to indent accordingly. - def to_json(state = nil, *) - state = State.from_state(state) - state.check_max_nesting - json_transform(state) - end - - private - - def json_shift(state) - state.object_nl.empty? or return '' - state.indent * state.depth - end - - def json_transform(state) - delim = ',' - delim << state.object_nl - result = '{' - result << state.object_nl - depth = state.depth += 1 - first = true - indent = !state.object_nl.empty? - each { |key,value| - result << delim unless first - result << state.indent * depth if indent - result << key.to_s.to_json(state) - result << state.space_before - result << ':' - result << state.space - result << value.to_json(state) - first = false - } - depth = state.depth -= 1 - result << state.object_nl - result << state.indent * depth if indent if indent - result << '}' - result - end - end - - module Array - # Returns a JSON string containing a JSON array, that is unparsed from - # this Array instance. - # _state_ is a JSON::State object, that can also be used to configure the - # produced JSON string output further. - def to_json(state = nil, *) - state = State.from_state(state) - state.check_max_nesting - json_transform(state) - end - - private - - def json_transform(state) - delim = ',' - delim << state.array_nl - result = '[' - result << state.array_nl - depth = state.depth += 1 - first = true - indent = !state.array_nl.empty? - each { |value| - result << delim unless first - result << state.indent * depth if indent - result << value.to_json(state) - first = false - } - depth = state.depth -= 1 - result << state.array_nl - result << state.indent * depth if indent - result << ']' - end - end - - module Integer - # Returns a JSON string representation for this Integer number. - def to_json(*) to_s end - end - - module Float - # Returns a JSON string representation for this Float number. - def to_json(state = nil, *) - state = State.from_state(state) - case - when infinite? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - when nan? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - else - to_s - end - end - end - - module String - if defined?(::Encoding) - # This string should be encoded with UTF-8 A call to this method - # returns a JSON string encoded with UTF16 big endian characters as - # \u????. - def to_json(state = nil, *args) - state = State.from_state(state) - if encoding == ::Encoding::UTF_8 - string = self - else - string = encode(::Encoding::UTF_8) - end - if state.ascii_only? - '"' << JSON.utf8_to_json_ascii(string) << '"' - else - '"' << JSON.utf8_to_json(string) << '"' - end - end - else - # This string should be encoded with UTF-8 A call to this method - # returns a JSON string encoded with UTF16 big endian characters as - # \u????. - def to_json(state = nil, *args) - state = State.from_state(state) - if state.ascii_only? - '"' << JSON.utf8_to_json_ascii(self) << '"' - else - '"' << JSON.utf8_to_json(self) << '"' - end - end - end - - # Module that holds the extinding methods if, the String module is - # included. - module Extend - # Raw Strings are JSON Objects (the raw bytes are stored in an - # array for the key "raw"). The Ruby String can be created by this - # module method. - def json_create(o) - o['raw'].pack('C*') - end - end - - # Extends _modul_ with the String::Extend module. - def self.included(modul) - modul.extend Extend - end - - # This method creates a raw object hash, that can be nested into - # other data structures and will be unparsed as a raw string. This - # method should be used, if you want to convert raw strings to JSON - # instead of UTF-8 strings, e. g. binary data. - def to_json_raw_object - { - JSON.create_id => self.class.name, - 'raw' => self.unpack('C*'), - } - end - - # This method creates a JSON text from the result of - # a call to to_json_raw_object of this String. - def to_json_raw(*args) - to_json_raw_object.to_json(*args) - end - end - - module TrueClass - # Returns a JSON string for true: 'true'. - def to_json(*) 'true' end - end - - module FalseClass - # Returns a JSON string for false: 'false'. - def to_json(*) 'false' end - end - - module NilClass - # Returns a JSON string for nil: 'null'. - def to_json(*) 'null' end - end - end - end - end -end - -module JSON - class << self - # If _object_ is string-like, parse the string and return the parsed result - # as a Ruby data structure. Otherwise generate a JSON text from the Ruby - # data structure object and return it. - # - # The _opts_ argument is passed through to generate/parse respectively. See - # generate and parse for their documentation. - def [](object, opts = {}) - if object.respond_to? :to_str - JSON.parse(object.to_str, opts) - else - JSON.generate(object, opts) - end - end - - # Returns the JSON parser class that is used by JSON. This is either - # JSON::Ext::Parser or JSON::Pure::Parser. - attr_reader :parser - - # Set the JSON parser class _parser_ to be used by JSON. - def parser=(parser) # :nodoc: - @parser = parser - remove_const :Parser if JSON.const_defined_in?(self, :Parser) - const_set :Parser, parser - end - - # Return the constant located at _path_. The format of _path_ has to be - # either ::A::B::C or A::B::C. In any case, A has to be located at the top - # level (absolute namespace path?). If there doesn't exist a constant at - # the given path, an ArgumentError is raised. - def deep_const_get(path) # :nodoc: - path.to_s.split(/::/).inject(Object) do |p, c| - case - when c.empty? then p - when JSON.const_defined_in?(p, c) then p.const_get(c) - else - begin - p.const_missing(c) - rescue NameError => e - raise ArgumentError, "can't get const #{path}: #{e}" - end - end - end - end - - # Set the module _generator_ to be used by JSON. - def generator=(generator) # :nodoc: - old, $VERBOSE = $VERBOSE, nil - @generator = generator - generator_methods = generator::GeneratorMethods - for const in generator_methods.constants - klass = deep_const_get(const) - modul = generator_methods.const_get(const) - klass.class_eval do - instance_methods(false).each do |m| - m.to_s == 'to_json' and remove_method m - end - include modul - end - end - self.state = generator::State - const_set :State, self.state - const_set :SAFE_STATE_PROTOTYPE, State.new - const_set :FAST_STATE_PROTOTYPE, State.new( - :indent => '', - :space => '', - :object_nl => "", - :array_nl => "", - :max_nesting => false - ) - const_set :PRETTY_STATE_PROTOTYPE, State.new( - :indent => ' ', - :space => ' ', - :object_nl => "\n", - :array_nl => "\n" - ) - ensure - $VERBOSE = old - end - - # Returns the JSON generator module that is used by JSON. This is - # either JSON::Ext::Generator or JSON::Pure::Generator. - attr_reader :generator - - # Returns the JSON generator state class that is used by JSON. This is - # either JSON::Ext::Generator::State or JSON::Pure::Generator::State. - attr_accessor :state - - # This is create identifier, which is used to decide if the _json_create_ - # hook of a class should be called. It defaults to 'json_class'. - attr_accessor :create_id - end - self.create_id = 'json_class' - - NaN = 0.0/0 - - Infinity = 1.0/0 - - MinusInfinity = -Infinity - - # The base exception for JSON errors. - class JSONError < StandardError; end - - # This exception is raised if a parser error occurs. - class ParserError < JSONError; end - - # This exception is raised if the nesting of parsed data structures is too - # deep. - class NestingError < ParserError; end - - # :stopdoc: - class CircularDatastructure < NestingError; end - # :startdoc: - - # This exception is raised if a generator or unparser error occurs. - class GeneratorError < JSONError; end - # For backwards compatibility - UnparserError = GeneratorError - - # This exception is raised if the required unicode support is missing on the - # system. Usually this means that the iconv library is not installed. - class MissingUnicodeSupport < JSONError; end - - module_function - - # Parse the JSON document _source_ into a Ruby data structure and return it. - # - # _opts_ can have the following - # keys: - # * *max_nesting*: The maximum depth of nesting allowed in the parsed data - # structures. Disable depth checking with :max_nesting => false. It defaults - # to 19. - # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in - # defiance of RFC 4627 to be parsed by the Parser. This option defaults - # to false. - # * *symbolize_names*: If set to true, returns symbols for the names - # (keys) in a JSON object. Otherwise strings are returned. Strings are - # the default. - # * *create_additions*: If set to false, the Parser doesn't create - # additions even if a matching class and create_id was found. This option - # defaults to true. - # * *object_class*: Defaults to Hash - # * *array_class*: Defaults to Array - def parse(source, opts = {}) - Parser.new(source, opts).parse - end - - # Parse the JSON document _source_ into a Ruby data structure and return it. - # The bang version of the parse method defaults to the more dangerous values - # for the _opts_ hash, so be sure only to parse trusted _source_ documents. - # - # _opts_ can have the following keys: - # * *max_nesting*: The maximum depth of nesting allowed in the parsed data - # structures. Enable depth checking with :max_nesting => anInteger. The parse! - # methods defaults to not doing max depth checking: This can be dangerous - # if someone wants to fill up your stack. - # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in - # defiance of RFC 4627 to be parsed by the Parser. This option defaults - # to true. - # * *create_additions*: If set to false, the Parser doesn't create - # additions even if a matching class and create_id was found. This option - # defaults to true. - def parse!(source, opts = {}) - opts = { - :max_nesting => false, - :allow_nan => true - }.update(opts) - Parser.new(source, opts).parse - end - - # Generate a JSON document from the Ruby data structure _obj_ and return - # it. _state_ is * a JSON::State object, - # * or a Hash like object (responding to to_hash), - # * an object convertible into a hash by a to_h method, - # that is used as or to configure a State object. - # - # It defaults to a state object, that creates the shortest possible JSON text - # in one line, checks for circular data structures and doesn't allow NaN, - # Infinity, and -Infinity. - # - # A _state_ hash can have the following keys: - # * *indent*: a string used to indent levels (default: ''), - # * *space*: a string that is put after, a : or , delimiter (default: ''), - # * *space_before*: a string that is put before a : pair delimiter (default: ''), - # * *object_nl*: a string that is put at the end of a JSON object (default: ''), - # * *array_nl*: a string that is put at the end of a JSON array (default: ''), - # * *allow_nan*: true if NaN, Infinity, and -Infinity should be - # generated, otherwise an exception is thrown if these values are - # encountered. This options defaults to false. - # * *max_nesting*: The maximum depth of nesting allowed in the data - # structures from which JSON is to be generated. Disable depth checking - # with :max_nesting => false, it defaults to 19. - # - # See also the fast_generate for the fastest creation method with the least - # amount of sanity checks, and the pretty_generate method for some - # defaults for pretty output. - def generate(obj, opts = nil) - if State === opts - state, opts = opts, nil - else - state = SAFE_STATE_PROTOTYPE.dup - end - if opts - if opts.respond_to? :to_hash - opts = opts.to_hash - elsif opts.respond_to? :to_h - opts = opts.to_h - else - raise TypeError, "can't convert #{opts.class} into Hash" - end - state = state.configure(opts) - end - state.generate(obj) - end - - # :stopdoc: - # I want to deprecate these later, so I'll first be silent about them, and - # later delete them. - alias unparse generate - module_function :unparse - # :startdoc: - - # Generate a JSON document from the Ruby data structure _obj_ and return it. - # This method disables the checks for circles in Ruby objects. - # - # *WARNING*: Be careful not to pass any Ruby data structures with circles as - # _obj_ argument because this will cause JSON to go into an infinite loop. - def fast_generate(obj, opts = nil) - if State === opts - state, opts = opts, nil - else - state = FAST_STATE_PROTOTYPE.dup - end - if opts - if opts.respond_to? :to_hash - opts = opts.to_hash - elsif opts.respond_to? :to_h - opts = opts.to_h - else - raise TypeError, "can't convert #{opts.class} into Hash" - end - state.configure(opts) - end - state.generate(obj) - end - - # :stopdoc: - # I want to deprecate these later, so I'll first be silent about them, and later delete them. - alias fast_unparse fast_generate - module_function :fast_unparse - # :startdoc: - - # Generate a JSON document from the Ruby data structure _obj_ and return it. - # The returned document is a prettier form of the document returned by - # #unparse. - # - # The _opts_ argument can be used to configure the generator. See the - # generate method for a more detailed explanation. - def pretty_generate(obj, opts = nil) - if State === opts - state, opts = opts, nil - else - state = PRETTY_STATE_PROTOTYPE.dup - end - if opts - if opts.respond_to? :to_hash - opts = opts.to_hash - elsif opts.respond_to? :to_h - opts = opts.to_h - else - raise TypeError, "can't convert #{opts.class} into Hash" - end - state.configure(opts) - end - state.generate(obj) - end - - # :stopdoc: - # I want to deprecate these later, so I'll first be silent about them, and later delete them. - alias pretty_unparse pretty_generate - module_function :pretty_unparse - # :startdoc: - - class << self - # The global default options for the JSON.load method: - # :max_nesting: false - # :allow_nan: true - # :quirks_mode: true - attr_accessor :load_default_options - end - self.load_default_options = { - :max_nesting => false, - :allow_nan => true, - :quirks_mode => true, - } - - # Load a ruby data structure from a JSON _source_ and return it. A source can - # either be a string-like object, an IO-like object, or an object responding - # to the read method. If _proc_ was given, it will be called with any nested - # Ruby object as an argument recursively in depth first order. The default - # options for the parser can be changed via the load_default_options method. - # - # This method is part of the implementation of the load/dump interface of - # Marshal and YAML. - def load(source, proc = nil) - opts = load_default_options - if source.respond_to? :to_str - source = source.to_str - elsif source.respond_to? :to_io - source = source.to_io.read - elsif source.respond_to?(:read) - source = source.read - end - if opts[:quirks_mode] && (source.nil? || source.empty?) - source = 'null' - end - result = parse(source, opts) - recurse_proc(result, &proc) if proc - result - end - - # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_ - def recurse_proc(result, &proc) - case result - when Array - result.each { |x| recurse_proc x, &proc } - proc.call result - when Hash - result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc } - proc.call result - else - proc.call result - end - end - - alias restore load - module_function :restore - - class << self - # The global default options for the JSON.dump method: - # :max_nesting: false - # :allow_nan: true - # :quirks_mode: true - attr_accessor :dump_default_options - end - self.dump_default_options = { - :max_nesting => false, - :allow_nan => true, - :quirks_mode => true, - } - - # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns - # the result. - # - # If anIO (an IO-like object or an object that responds to the write method) - # was given, the resulting JSON is written to it. - # - # If the number of nested arrays or objects exceeds _limit_, an ArgumentError - # exception is raised. This argument is similar (but not exactly the - # same!) to the _limit_ argument in Marshal.dump. - # - # The default options for the generator can be changed via the - # dump_default_options method. - # - # This method is part of the implementation of the load/dump interface of - # Marshal and YAML. - def dump(obj, anIO = nil, limit = nil) - if anIO and limit.nil? - anIO = anIO.to_io if anIO.respond_to?(:to_io) - unless anIO.respond_to?(:write) - limit = anIO - anIO = nil - end - end - opts = JSON.dump_default_options - limit and opts.update(:max_nesting => limit) - result = generate(obj, opts) - if anIO - anIO.write result - anIO - else - result - end - rescue JSON::NestingError - raise ArgumentError, "exceed depth limit" - end - - # Swap consecutive bytes of _string_ in place. - def self.swap!(string) # :nodoc: - 0.upto(string.size / 2) do |i| - break unless string[2 * i + 1] - string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i] - end - string - end - - # Shortuct for iconv. - if ::String.method_defined?(:encode) - # Encodes string using Ruby's _String.encode_ - def self.iconv(to, from, string) - string.encode(to, from) - end - else - require 'iconv' - # Encodes string using _iconv_ library - def self.iconv(to, from, string) - Iconv.conv(to, from, string) - end - end - - if ::Object.method(:const_defined?).arity == 1 - def self.const_defined_in?(modul, constant) - modul.const_defined?(constant) - end - else - def self.const_defined_in?(modul, constant) - modul.const_defined?(constant, false) - end - end -end - -module ::Kernel - private - - # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in - # one line. - def j(*objs) - objs.each do |obj| - puts JSON::generate(obj, :allow_nan => true, :max_nesting => false) - end - nil - end - - # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with - # indentation and over many lines. - def jj(*objs) - objs.each do |obj| - puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) - end - nil - end - - # If _object_ is string-like, parse the string and return the parsed result as - # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data - # structure object and return it. - # - # The _opts_ argument is passed through to generate/parse respectively. See - # generate and parse for their documentation. - def JSON(object, *args) - if object.respond_to? :to_str - JSON.parse(object.to_str, args.first) - else - JSON.generate(object, args.first) - end - end -end - -# Extends any Class to include _json_creatable?_ method. -class ::Class - # Returns true if this class can be used to create an instance - # from a serialised JSON string. The class has to implement a class - # method _json_create_ that expects a hash as first parameter. The hash - # should include the required data. - def json_creatable? - respond_to?(:json_create) - end -end - -JSON.generator = JSON::Pure::Generator -JSON.parser = JSON::Pure::Parser -module GHI - module Formatting - module Colors - class << self - attr_accessor :colorize - def colorize? - return @colorize if defined? @colorize - @colorize = STDOUT.tty? - end - end - - def colorize? - Colors.colorize? - end - - def fg color, &block - escape color, 3, &block - end - - def bg color, &block - fg(offset(color)) { escape color, 4, &block } - end - - def bright &block - escape :bright, &block - end - - def underline &block - escape :underline, &block - end - - def blink &block - escape :blink, &block - end - - def inverse &block - escape :inverse, &block - end - - def no_color - old_colorize, Colors.colorize = colorize?, false - yield - ensure - Colors.colorize = old_colorize - end - - def to_hex string - WEB[string] || string.downcase.sub(/^(#|0x)/, ''). - sub(/^([0-f])([0-f])([0-f])$/, '\1\1\2\2\3\3') - end - - ANSI = { - :bright => 1, - :underline => 4, - :blink => 5, - :inverse => 7, - - :black => 0, - :red => 1, - :green => 2, - :yellow => 3, - :blue => 4, - :magenta => 5, - :cyan => 6, - :white => 7 - } - - WEB = { - 'aliceblue' => 'f0f8ff', - 'antiquewhite' => 'faebd7', - 'aqua' => '00ffff', - 'aquamarine' => '7fffd4', - 'azure' => 'f0ffff', - 'beige' => 'f5f5dc', - 'bisque' => 'ffe4c4', - 'black' => '000000', - 'blanchedalmond' => 'ffebcd', - 'blue' => '0000ff', - 'blueviolet' => '8a2be2', - 'brown' => 'a52a2a', - 'burlywood' => 'deb887', - 'cadetblue' => '5f9ea0', - 'chartreuse' => '7fff00', - 'chocolate' => 'd2691e', - 'coral' => 'ff7f50', - 'cornflowerblue' => '6495ed', - 'cornsilk' => 'fff8dc', - 'crimson' => 'dc143c', - 'cyan' => '00ffff', - 'darkblue' => '00008b', - 'darkcyan' => '008b8b', - 'darkgoldenrod' => 'b8860b', - 'darkgray' => 'a9a9a9', - 'darkgrey' => 'a9a9a9', - 'darkgreen' => '006400', - 'darkkhaki' => 'bdb76b', - 'darkmagenta' => '8b008b', - 'darkolivegreen' => '556b2f', - 'darkorange' => 'ff8c00', - 'darkorchid' => '9932cc', - 'darkred' => '8b0000', - 'darksalmon' => 'e9967a', - 'darkseagreen' => '8fbc8f', - 'darkslateblue' => '483d8b', - 'darkslategray' => '2f4f4f', - 'darkslategrey' => '2f4f4f', - 'darkturquoise' => '00ced1', - 'darkviolet' => '9400d3', - 'deeppink' => 'ff1493', - 'deepskyblue' => '00bfff', - 'dimgray' => '696969', - 'dimgrey' => '696969', - 'dodgerblue' => '1e90ff', - 'firebrick' => 'b22222', - 'floralwhite' => 'fffaf0', - 'forestgreen' => '228b22', - 'fuchsia' => 'ff00ff', - 'gainsboro' => 'dcdcdc', - 'ghostwhite' => 'f8f8ff', - 'gold' => 'ffd700', - 'goldenrod' => 'daa520', - 'gray' => '808080', - 'green' => '008000', - 'greenyellow' => 'adff2f', - 'honeydew' => 'f0fff0', - 'hotpink' => 'ff69b4', - 'indianred' => 'cd5c5c', - 'indigo' => '4b0082', - 'ivory' => 'fffff0', - 'khaki' => 'f0e68c', - 'lavender' => 'e6e6fa', - 'lavenderblush' => 'fff0f5', - 'lawngreen' => '7cfc00', - 'lemonchiffon' => 'fffacd', - 'lightblue' => 'add8e6', - 'lightcoral' => 'f08080', - 'lightcyan' => 'e0ffff', - 'lightgoldenrodyellow' => 'fafad2', - 'lightgreen' => '90ee90', - 'lightgray' => 'd3d3d3', - 'lightgrey' => 'd3d3d3', - 'lightpink' => 'ffb6c1', - 'lightsalmon' => 'ffa07a', - 'lightseagreen' => '20b2aa', - 'lightskyblue' => '87cefa', - 'lightslategray' => '778899', - 'lightslategrey' => '778899', - 'lightsteelblue' => 'b0c4de', - 'lightyellow' => 'ffffe0', - 'lime' => '00ff00', - 'limegreen' => '32cd32', - 'linen' => 'faf0e6', - 'magenta' => 'ff00ff', - 'maroon' => '800000', - 'mediumaquamarine' => '66cdaa', - 'mediumblue' => '0000cd', - 'mediumorchid' => 'ba55d3', - 'mediumpurple' => '9370db', - 'mediumseagreen' => '3cb371', - 'mediumslateblue' => '7b68ee', - 'mediumspringgreen' => '00fa9a', - 'mediumturquoise' => '48d1cc', - 'mediumvioletred' => 'c71585', - 'midnightblue' => '191970', - 'mintcream' => 'f5fffa', - 'mistyrose' => 'ffe4e1', - 'moccasin' => 'ffe4b5', - 'navajowhite' => 'ffdead', - 'navy' => '000080', - 'oldlace' => 'fdf5e6', - 'olive' => '808000', - 'olivedrab' => '6b8e23', - 'orange' => 'ffa500', - 'orangered' => 'ff4500', - 'orchid' => 'da70d6', - 'palegoldenrod' => 'eee8aa', - 'palegreen' => '98fb98', - 'paleturquoise' => 'afeeee', - 'palevioletred' => 'db7093', - 'papayawhip' => 'ffefd5', - 'peachpuff' => 'ffdab9', - 'peru' => 'cd853f', - 'pink' => 'ffc0cb', - 'plum' => 'dda0dd', - 'powderblue' => 'b0e0e6', - 'purple' => '800080', - 'red' => 'ff0000', - 'rosybrown' => 'bc8f8f', - 'royalblue' => '4169e1', - 'saddlebrown' => '8b4513', - 'salmon' => 'fa8072', - 'sandybrown' => 'f4a460', - 'seagreen' => '2e8b57', - 'seashell' => 'fff5ee', - 'sienna' => 'a0522d', - 'silver' => 'c0c0c0', - 'skyblue' => '87ceeb', - 'slateblue' => '6a5acd', - 'slategray' => '708090', - 'slategrey' => '708090', - 'snow' => 'fffafa', - 'springgreen' => '00ff7f', - 'steelblue' => '4682b4', - 'tan' => 'd2b48c', - 'teal' => '008080', - 'thistle' => 'd8bfd8', - 'tomato' => 'ff6347', - 'turquoise' => '40e0d0', - 'violet' => 'ee82ee', - 'wheat' => 'f5deb3', - 'white' => 'ffffff', - 'whitesmoke' => 'f5f5f5', - 'yellow' => 'ffff00', - 'yellowgreen' => '9acd32' - } - - private - - def escape color = :black, layer = nil - return yield unless color && colorize? - previous_escape = Thread.current[:escape] || "\e[0m" - escape = Thread.current[:escape] = "\e[%s%sm" % [ - layer, ANSI[color] || "8;5;#{to_256(*to_rgb(color))}" - ] - [escape, yield, previous_escape].join - ensure - Thread.current[:escape] = previous_escape - end - - def to_256 r, g, b - r, g, b = [r, g, b].map { |c| c / 10 } - return 232 + g if r == g && g == b && g != 0 && g != 25 - 16 + ((r / 5) * 36) + ((g / 5) * 6) + (b / 5) - end - - def to_rgb hex - n = (WEB[hex.to_s] || hex).to_i(16) - [2, 1, 0].map { |m| n >> (m << 3) & 0xff } - end - - def offset hex - h, s, l = rgb_to_hsl(to_rgb(WEB[hex.to_s] || hex)) - l < 55 && !(40..80).include?(h) ? l *= 1.875 : l /= 3 - hsl_to_rgb([h, s, l]).map { |c| '%02x' % c }.join - end - - def rgb_to_hsl rgb - r, g, b = rgb.map { |c| c / 255.0 } - max = [r, g, b].max - min = [r, g, b].min - d = max - min - h = case max - when min then 0 - when r then 60 * (g - b) / d - when g then 60 * (b - r) / d + 120 - when b then 60 * (r - g) / d + 240 - end - l = (max + min) / 2.0 - s = if max == min then 0 - elsif l < 0.5 then d / (2 * l) - else d / (2 - 2 * l) - end - [h % 360, s * 100, l * 100] - end - - def hsl_to_rgb hsl - h, s, l = hsl - h /= 360.0 - s /= 100.0 - l /= 100.0 - m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s - m1 = l * 2 - m2 - rgb = [[m1, m2, h + 1.0 / 3], [m1, m2, h], [m1, m2, h - 1.0 / 3]] - rgb.map { |c| - m1, m2, h = c - h += 1 if h < 0 - h -= 1 if h > 1 - next m1 + (m2 - m1) * h * 6 if h * 6 < 1 - next m2 if h * 2 < 1 - next m1 + (m2 - m1) * (2.0/3 - h) * 6 if h * 3 < 2 - m1 - }.map { |c| c * 255 } - end - - def hue_to_rgb m1, m2, h - h += 1 if h < 0 - h -= 1 if h > 1 - return m1 + (m2 - m1) * h * 6 if h * 6 < 1 - return m2 if h * 2 < 1 - return m1 + (m2 - m1) * (2.0/3 - h) * 6 if h * 3 < 2 - return m1 - end - end - end -end -# encoding: utf-8 -require 'date' -require 'erb' - -module GHI - module Formatting - class << self - attr_accessor :paginate - end - self.paginate = true # Default. - include Colors - - CURSOR = { - :up => lambda { |n| "\e[#{n}A" }, - :column => lambda { |n| "\e[#{n}G" }, - :hide => "\e[?25l", - :show => "\e[?25h" - } - - THROBBERS = [ - %w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏), - %w(⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓), - %w(⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆ ), - %w(⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋), - %w(⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁), - %w(⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈), - %w(⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈ ⠉) - ] - - def puts *strings - strings = strings.flatten.map { |s| - s.gsub(/@([^@\s]+)/) { - if $1 == Authorization.username - bright { fg(:yellow) { "@#$1" } } - else - bright { "@#$1" } - end - } - } - super strings - end - - def page header = nil, throttle = 0 - if paginate? - pager = GHI.config('ghi.pager') || GHI.config('core.pager') - pager ||= ENV['PAGER'] - pager ||= 'less' - pager += ' -EKRX -b1' if pager =~ /^less( -[EKRX]+)?$/ - - if pager && !pager.empty? && pager != 'cat' - $stdout = IO.popen pager, 'w' - end - - puts header if header - end - - loop do - yield - sleep throttle - end - rescue Errno::EPIPE - exit - ensure - unless $stdout == STDOUT - $stdout.close_write - $stdout = STDOUT - print CURSOR[:show] - exit - end - end - - def paginate? - $stdout.tty? && $stdout == STDOUT && Formatting.paginate - end - - def truncate string, reserved - result = string.scan(/.{0,#{columns - reserved}}(?:\s|\Z)/).first.strip - result << "..." if result != string - result - end - - def indent string, level = 4, maxwidth = columns - string = string.gsub(/\r/, '') - string.gsub!(/[\t ]+$/, '') - string.gsub!(/\n{3,}/, "\n\n") - width = maxwidth - level - 1 - lines = string.scan( - /.{0,#{width}}(?:\s|\Z)|[\S]{#{width},}/ # TODO: Test long lines. - ).map { |line| " " * level + line.chomp } - format_markdown lines.join("\n").rstrip, level - end - - def columns - dimensions[1] || 80 - end - - def dimensions - `stty size`.chomp.split(' ').map { |n| n.to_i } - end - - #-- - # Specific formatters: - #++ - - def format_issues_header - state = assigns[:state] || 'open' - header = "# #{repo || 'Global,'} #{state} issues" - if repo - if milestone = assigns[:milestone] - case milestone - when '*' then header << ' with a milestone' - when 'none' then header << ' without a milestone' - else - header.sub! repo, "#{repo} milestone ##{milestone}" - end - end - if assignee = assigns[:assignee] - header << case assignee - when '*' then ', assigned' - when 'none' then ', unassigned' - else - assignee = 'you' if Authorization.username == assignee - ", assigned to #{assignee}" - end - end - if mentioned = assigns[:mentioned] - mentioned = 'you' if Authorization.username == mentioned - header << ", mentioning #{mentioned}" - end - else - header << case assigns[:filter] - when 'created' then ' you created' - when 'mentioned' then ' that mention you' - when 'subscribed' then " you're subscribed to" - else - ' assigned to you' - end - end - if labels = assigns[:labels] - header << ", labeled #{assigns[:labels].gsub ',', ', '}" - end - if sort = assigns[:sort] - header << ", by #{sort} #{reverse ? 'ascending' : 'descending'}" - end - format_state assigns[:state], header - end - - # TODO: Show milestones. - def format_issues issues, include_repo - return 'None.' if issues.empty? - - include_repo and issues.each do |i| - %r{/repos/[^/]+/([^/]+)} === i['url'] and i['repo'] = $1 - end - - nmax, rmax = %w(number repo).map { |f| - issues.sort_by { |i| i[f].to_s.size }.last[f].to_s.size - } - - issues.map { |i| - n, title, labels = i['number'], i['title'], i['labels'] - l = 9 + nmax + rmax + no_color { format_labels labels }.to_s.length - a = i['assignee'] && i['assignee']['login'] == Authorization.username - l += 2 if a - p = i['pull_request']['html_url'] and l += 2 - c = i['comments'] - l += c.to_s.length + 1 unless c == 0 - [ - " ", - (i['repo'].to_s.rjust(rmax) if i['repo']), - "#{bright { n.to_s.rjust nmax }}:", - truncate(title, l), - format_labels(labels), - (fg('aaaaaa') { c } unless c == 0), - (fg('aaaaaa') { '↑' } if p), - (fg(:yellow) { '@' } if a) - ].compact.join ' ' - } - end - - # TODO: Show milestone, number of comments, pull request attached. - def format_issue i, width = columns - return unless i['created_at'] - ERB.new(<\ -<%= bright { no_color { indent '%s%s: %s' % [p ? '↑' : '#', \ -*i.values_at('number', 'title')], 0, width } } %> -@<%= i['user']['login'] %> opened this <%= p ? 'pull request' : 'issue' %> \ -<%= format_date DateTime.parse(i['created_at']) %>. \ -<%= format_state i['state'], format_tag(i['state']), :bg %> \ -<% unless i['comments'] == 0 %>\ -<%= fg('aaaaaa'){ - template = "%d comment" - template << "s" unless i['comments'] == 1 - '(' << template % i['comments'] << ')' -} %>\ -<% end %>\ -<% if i['assignee'] || !i['labels'].empty? %> -<% if i['assignee'] %>@<%= i['assignee']['login'] %> is assigned. <% end %>\ -<% unless i['labels'].empty? %><%= format_labels(i['labels']) %><% end %>\ -<% end %>\ -<% if i['milestone'] %> -Milestone #<%= i['milestone']['number'] %>: <%= i['milestone']['title'] %>\ -<%= " \#{bright{fg(:yellow){'⚠'}}}" if past_due? i['milestone'] %>\ -<% end %> -<% if i['body'] && !i['body'].empty? %> -<%= indent i['body'], 4, width %> -<% end %> - -EOF - end - - def format_comments comments - return 'None.' if comments.empty? - comments.map { |comment| format_comment comment } - end - - def format_comment c, width = columns - < -@<%= m['creator']['login'] %> created this milestone \ -<%= format_date DateTime.parse(m['created_at']) %>. \ -<%= format_state m['state'], format_tag(m['state']), :bg %> -<% if m['due_on'] %>\ -<% due_on = DateTime.parse m['due_on'] %>\ -<% if past_due? m %>\ -<%= bright{fg(:yellow){"⚠"}} %> \ -<%= bright{fg(:red){"Past due by \#{format_date due_on, false}."}} %> -<% else %>\ -Due in <%= format_date due_on, false %>. -<% end %>\ -<% end %>\ -<%= percent m %> -<% if m['description'] && !m['description'].empty? %> -<%= indent m['description'], 4, width %> -<% end %> - -EOF - end - - def past_due? milestone - return false unless milestone['due_on'] - DateTime.parse(milestone['due_on']) <= DateTime.now - end - - def percent milestone, string = nil - open, closed = milestone.values_at('open_issues', 'closed_issues') - complete = closed.to_f / (open + closed) - complete = 0 if complete.nan? - i = (columns * complete).round - if string.nil? - string = ' %d%% (%d closed, %d open)' % [complete * 100, closed, open] - end - string = string.ljust columns - [bg('2cc200'){string[0, i]}, string[i, columns - i]].join - end - - def format_state state, string = state, layer = :fg - send(layer, state == 'closed' ? 'ff0000' : '2cc200') { string } - end - - def format_labels labels - return if labels.empty? - [*labels].map { |l| bg(l['color']) { format_tag l['name'] } }.join ' ' - end - - def format_tag tag - (colorize? ? ' %s ' : '[%s]') % tag - end - - #-- - # Helpers: - #++ - - #-- - # TODO: DRY up editor formatters. - #++ - def format_editor issue = nil - message = ERB.new(< - -<%= no_color { format_issue issue, columns - 2 if issue } %> -EOF - message.rstrip! - message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip } - message.insert 0, [ - issue['title'] || issue[:title], issue['body'] || issue[:body] - ].compact.join("\n\n") if issue - message - end - - def format_milestone_editor milestone = nil - message = ERB.new(< - -<%= no_color { format_milestone milestone, columns - 2 } if milestone %> -EOF - message.rstrip! - message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip } - message.insert 0, [ - milestone['title'], milestone['description'] - ].join("\n\n") if milestone - message - end - - def format_comment_editor issue, comment = nil - message = ERB.new(< issue #<%= issue['number'] %> - -<%= no_color { format_issue issue } if verbose %>\ -<%= no_color { format_comment comment, columns - 2 } if comment %> -EOF - message.rstrip! - message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip } - message.insert 0, comment['body'] if comment - message - end - - def format_markdown string, indent = 4 - c = '268bd2' - - # Headers. - string.gsub!(/^( {#{indent}}\#{1,6} .+)$/, bright{'\1'}) - string.gsub!( - /(^ {#{indent}}.+$\n^ {#{indent}}[-=]+$)/, bright{'\1'} - ) - # Strong. - string.gsub!( - /(^|\s)(\*{2}\w(?:[^*]*\w)?\*{2})(\s|$)/m, '\1' + bright{'\2'} + '\3' - ) - string.gsub!( - /(^|\s)(_{2}\w(?:[^_]*\w)?_{2})(\s|$)/m, '\1' + bright {'\2'} + '\3' - ) - # Emphasis. - string.gsub!( - /(^|\s)(\*\w(?:[^*]*\w)?\*)(\s|$)/m, '\1' + underline{'\2'} + '\3' - ) - string.gsub!( - /(^|\s)(_\w(?:[^_]*\w)?_)(\s|$)/m, '\1' + underline{'\2'} + '\3' - ) - # Bullets/Blockquotes. - string.gsub!(/(^ {#{indent}}(?:[*>-]|\d+\.) )/, fg(c){'\1'}) - # URIs. - string.gsub!( - %r{\b(<)?(https?://\S+|[^@\s]+@[^@\s]+)(>)?\b}, - fg(c){'\1' + underline{'\2'} + '\3'} - ) - # Code. - # string.gsub!( - # / - # (^\ {#{indent}}```.*?$)(.+?^\ {#{indent}}```$)| - # (^|[^`])(`[^`]+`)([^`]|$) - # /mx - # ) { - # post = $5 - # fg(c){"#$1#$2#$3#$4".gsub(/\e\[[\d;]+m/, '')} + "#{post}" - # } - string - end - - def format_date date, suffix = true - days = (interval = DateTime.now - date).to_i.abs - string = if days.zero? - seconds, _ = interval.divmod Rational(1, 86400) - hours, seconds = seconds.divmod 3600 - minutes, seconds = seconds.divmod 60 - if hours > 0 - "#{hours} hour#{'s' unless hours == 1}" - elsif minutes > 0 - "#{minutes} minute#{'s' unless minutes == 1}" - else - "#{seconds} second#{'s' unless seconds == 1}" - end - else - "#{days} day#{'s' unless days == 1}" - end - ago = interval < 0 ? 'from now' : 'ago' if suffix - [string, ago].compact.join ' ' - end - - def throb position = 0, redraw = CURSOR[:up][1] - return yield unless paginate? - - throb = THROBBERS[rand(THROBBERS.length)] - throb.reverse! if rand > 0.5 - i = rand throb.length - - thread = Thread.new do - dot = lambda do - print "\r#{CURSOR[:column][position]}#{throb[i]}#{CURSOR[:hide]}" - i = (i + 1) % throb.length - sleep 0.1 and dot.call - end - dot.call - end - yield - ensure - if thread - thread.kill - puts "\r#{CURSOR[:column][position]}#{redraw}#{CURSOR[:show]}" - end - end - end -end -# encoding: utf-8 - -module GHI - module Authorization - extend Formatting - - class Required < RuntimeError - def message() 'Authorization required.' end - end - - class << self - def token - return @token if defined? @token - @token = GHI.config 'ghi.token' - end - - def authorize! user = username, pass = password, local = true - return false unless user && pass - - res = throb(54, "✔\r") { - Client.new(user, pass).post( - '/authorizations', - :scopes => %w(public_repo repo), - :note => 'ghi', - :note_url => 'https://github.com/stephencelis/ghi' - ) - } - @token = res.body['token'] - - run = [] - unless username - run << "git config#{' --global' unless local} github.user #{user}" - end - run << "git config#{' --global' unless local} ghi.token #{token}" - - system run.join('; ') - - unless local - at_exit do - warn < e - abort "#{e.message}#{CURSOR[:column][0]}" - end - - def username - return @username if defined? @username - @username = GHI.config 'github.user' - end - - def password - return @password if defined? @password - @password = GHI.config 'github.password' - end - end - end -end -require 'cgi' -require 'net/https' - -unless defined? Net::HTTP::Patch - # PATCH support for 1.8.7. - Net::HTTP::Patch = Class.new(Net::HTTP::Post) { METHOD = 'PATCH' } -end - -module GHI - class Client - class Error < RuntimeError - attr_reader :response - def initialize response - @response, @json = response, JSON.parse(response.body) - end - - def body() @json end - def message() body['message'] end - def errors() [*body['errors']] end - end - - class Response - def initialize response - @response = response - end - - def body - @body ||= JSON.parse @response.body - end - - def next_page() links['next'] end - def last_page() links['last'] end - - private - - def links - return @links if defined? @links - @links = {} - if links = @response['Link'] - links.scan(/<([^>]+)>; rel="([^"]+)"/).each { |l, r| @links[r] = l } - end - @links - end - end - - CONTENT_TYPE = 'application/vnd.github+json' - METHODS = { - :head => Net::HTTP::Head, - :get => Net::HTTP::Get, - :post => Net::HTTP::Post, - :put => Net::HTTP::Put, - :patch => Net::HTTP::Patch, - :delete => Net::HTTP::Delete - } - - attr_reader :username, :password - def initialize username = nil, password = nil - @username, @password = username, password - end - - def head path, options = {} - request :head, path, options - end - - def get path, params = {}, options = {} - request :get, path, options.merge(:params => params) - end - - def post path, body = nil, options = {} - request :post, path, options.merge(:body => body) - end - - def put path, body = nil, options = {} - request :put, path, options.merge(:body => body) - end - - def patch path, body = nil, options = {} - request :patch, path, options.merge(:body => body) - end - - def delete path, options = {} - request :delete, path, options - end - - private - - def request method, path, options - if params = options[:params] and !params.empty? - q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" } - path += "?#{q.join '&'}" - end - - req = METHODS[method].new path, 'Accept' => CONTENT_TYPE - if GHI::Authorization.token - req['Authorization'] = "token #{GHI::Authorization.token}" - end - if options.key? :body - req['Content-Type'] = CONTENT_TYPE - req.body = options[:body] ? JSON.dump(options[:body]) : '' - end - req.basic_auth username, password if username && password - - http = Net::HTTP.new 'api.github.com', 443 - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7 - - GHI.v? and puts "\r===> #{method.to_s.upcase} #{path} #{req.body}" - res = http.start { http.request req } - GHI.v? and puts "\r<=== #{res.code}: #{res.body}" - - case res - when Net::HTTPSuccess - return Response.new(res) - when Net::HTTPUnauthorized - if password.nil? - raise Authorization::Required, 'Authorization required' - end - end - - raise Error, res - end - end -end -require 'tmpdir' - -module GHI - class Editor - attr_reader :filename - def initialize filename - @filename = filename - end - - def gets prefill - File.open path, 'a+' do |f| - f << prefill if File.zero? path - f.rewind - system "#{editor} #{f.path}" - return File.read(f.path).gsub(/(?:^#.*$\n?)+\s*\z/, '').strip - end - end - - def unlink message = nil - File.delete path - abort message if message - end - - private - - def editor - editor = GHI.config 'ghi.editor' - editor ||= GHI.config 'core.editor' - editor ||= ENV['VISUAL'] - editor ||= ENV['EDITOR'] - editor ||= 'vi' - end - - def path - File.join dir, filename - end - - def dir - @dir ||= git_dir || Dir.tmpdir - end - - def git_dir - return unless Commands::Command.detected_repo - dir = `git rev-parse --git-dir 2>/dev/null`.chomp - dir unless dir.empty? - end - end -end -require 'uri' - -module GHI - class Web - BASE_URI = 'https://github.com/' - - attr_reader :base - def initialize base - @base = base - end - - def open path = '', params = {} - unless params.empty? - q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" } - path += "?#{q.join '&'}" - end - system "open '#{uri + path}'" - end - - private - - def uri - URI(BASE_URI) + "#{base}/" - end - end -end -module GHI - module Commands - end -end -module GHI - module Commands - class MissingArgument < RuntimeError - end - - class Command - include Formatting - - class << self - attr_accessor :detected_repo - - def execute args - command = new args - if i = args.index('--') - command.repo = args.slice!(i, args.length)[1] # Raise if too many? - end - command.execute - end - end - - attr_reader :args - attr_writer :issue - attr_accessor :action - attr_accessor :verbose - - def initialize args - @args = args.map! { |a| a.dup } - end - - def assigns - @assigns ||= {} - end - - def api - @api ||= Client.new - end - - def repo - return @repo if defined? @repo - @repo = GHI.config('ghi.repo') || detect_repo - if @repo && !@repo.include?('/') - @repo = [Authorization.username, @repo].join '/' - end - @repo - end - alias extract_repo repo - - def repo= repo - @repo = repo.dup - unless @repo.include? '/' - @repo.insert 0, "#{Authorization.username}/" - end - @repo - end - - private - - def require_repo - return true if repo - warn 'Not a GitHub repo.' - warn '' - abort options.to_s - end - - def detect_repo - remote = remotes.find { |r| r[:remote] == 'upstream' } - remote ||= remotes.find { |r| r[:remote] == 'origin' } - remote ||= remotes.find { |r| r[:user] == Authorization.username } - Command.detected_repo = true and remote[:repo] if remote - end - - def remotes - return @remotes if defined? @remotes - @remotes = `git config --get-regexp remote\..+\.url`.split "\n" - @remotes.reject! { |r| !r.include? 'github.com'} - @remotes.map! { |r| - remote, user, repo = r.scan( - %r{remote\.([^\.]+)\.url .*?([^:/]+)/([^/\s]+?)(?:\.git)?$} - ).flatten - { :remote => remote, :user => user, :repo => "#{user}/#{repo}" } - } - @remotes - end - - def issue - return @issue if defined? @issue - index = args.index { |arg| /^\d+$/ === arg } - @issue = (args.delete_at index if index) - end - alias extract_issue issue - alias milestone issue - alias extract_milestone issue - - def require_issue - raise MissingArgument, 'Issue required.' unless issue - end - - def require_milestone - raise MissingArgument, 'Milestone required.' unless milestone - end - - # Handles, e.g. `--[no-]milestone []`. - def any_or_none_or input - input ? input : { nil => '*', false => 'none' }[input] - end - end - end -end -module GHI - module Commands - class Assign < Command - def options - OptionParser.new do |opts| - opts.banner = <] - or: ghi assign - or: ghi unassign -EOF - opts.separator '' - opts.on( - '-u', '--assignee ', 'assign to specified user' - ) do |assignee| - assigns[:assignee] = assignee - end - opts.on '-d', '--no-assignee', 'unassign this issue' do - assigns[:assignee] = nil - end - opts.on '-l', '--list', 'list assigned issues' do - self.action = 'list' - end - opts.separator '' - end - end - - def execute - self.action = 'edit' - assigns[:args] = [] - - require_repo - extract_issue - options.parse! args - - unless assigns.key? :assignee - assigns[:assignee] = args.pop || Authorization.username - end - if assigns.key? :assignee - assigns[:args].concat( - assigns[:assignee] ? %W(-u #{assigns[:assignee]}) : %w(--no-assign) - ) - end - assigns[:args] << issue if issue - assigns[:args].concat %W(-- #{repo}) - - case action - when 'list' then List.execute assigns[:args] - when 'edit' then Edit.execute assigns[:args] - end - end - end - end -end -module GHI - module Commands - class Close < Command - attr_accessor :web - - def options - OptionParser.new do |opts| - opts.banner = < -EOF - opts.separator '' - opts.on '-l', '--list', 'list closed issues' do - assigns[:command] = List - end - opts.on('-w', '--web') { self.web = true } - opts.separator '' - opts.separator 'Issue modification options' - opts.on '-m', '--message []', 'close with message' do |text| - assigns[:comment] = text - end - opts.separator '' - end - end - - def execute - options.parse! args - require_repo - - if list? - args.unshift(*%W(-sc -- #{repo})) - args.unshift '-w' if web - List.execute args - else - require_issue - if assigns.key? :comment - Comment.execute [ - issue, '-m', assigns[:comment], '--', repo - ].compact - end - Edit.execute %W(-sc #{issue} -- #{repo}) - end - end - - private - - def list? - assigns[:command] == List - end - end - end -end -module GHI - module Commands - class Comment < Command - attr_accessor :comment - attr_accessor :verbose - attr_accessor :web - - def options - OptionParser.new do |opts| - opts.banner = < -EOF - opts.separator '' - opts.on '-l', '--list', 'list comments' do - self.action = 'list' - end - opts.on('-w', '--web') { self.web = true } - # opts.on '-v', '--verbose', 'list events, too' - opts.separator '' - opts.separator 'Comment modification options' - opts.on '-m', '--message []', 'comment body' do |text| - assigns[:body] = text - end - opts.on '--amend', 'amend previous comment' do - self.action = 'update' - end - opts.on '-D', '--delete', 'delete previous comment' do - self.action = 'destroy' - end - opts.on '--close', 'close associated issue' do - self.action = 'close' - end - opts.on '-v', '--verbose' do - self.verbose = true - end - opts.separator '' - end - end - - def execute - require_issue - require_repo - self.action ||= 'create' - options.parse! args - - case action - when 'list' - res = index - page do - puts format_comments(res.body) - break unless res.next_page - res = throb { api.get res.next_page } - end - when 'create' - if web - Web.new(repo).open "issues/#{issue}#issue_comment_form" - else - create - end - when 'update', 'destroy' - res = index - res = throb { api.get res.last_page } if res.last_page - self.comment = res.body.reverse.find { |c| - c['user']['login'] == Authorization.username - } - if comment - send action - else - abort 'No recent comment found.' - end - when 'close' - Close.execute [issue, '-m', assigns[:body], '--', repo].compact - end - end - - protected - - def index - throb { api.get uri, :per_page => 100 } - end - - def create message = 'Commented.' - e = require_body - c = throb { api.post uri, assigns }.body - puts format_comment(c) - puts message - e.unlink if e - end - - def update - create 'Comment updated.' - end - - def destroy - throb { api.delete uri } - puts 'Comment deleted.' - end - - private - - def uri - if comment - comment['url'] - else - "/repos/#{repo}/issues/#{issue}/comments" - end - end - - def require_body - assigns[:body] = args.join ' ' unless args.empty? - return if assigns[:body] - if issue && verbose - i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body - else - i = {'number'=>issue} - end - filename = "GHI_COMMENT_#{issue}" - filename << "_#{comment['id']}" if comment - e = Editor.new filename - message = e.gets format_comment_editor(i, comment) - e.unlink 'No comment.' if message.nil? || message.empty? - if comment && message.strip == comment['body'].strip - e.unlink 'No change.' - end - assigns[:body] = message if message - e - end - end - end -end -module GHI - module Commands - class Config < Command - def options - OptionParser.new do |opts| - opts.banner = <]' do |username| - self.action = 'auth' - assigns[:username] = username || Authorization.username - end - opts.separator '' - end - end - - def execute - global = true - options.parse! args.empty? ? %w(-h) : args - - if self.action == 'auth' - assigns[:password] = Authorization.password || get_password - Authorization.authorize!( - assigns[:username], assigns[:password], assigns[:local] - ) - end - end - - private - - def get_password - print "Enter #{assigns[:username]}'s GitHub password (never stored): " - current_tty = `stty -g` - system 'stty raw -echo -icanon isig' if $?.success? - input = '' - while char = $stdin.getbyte and not (char == 13 or char == 10) - if char == 127 or char == 8 - input[-1, 1] = '' unless input.empty? - else - input << char.chr - end - end - input - rescue Interrupt - print '^C' - ensure - system "stty #{current_tty}" unless current_tty.empty? - end - end - end -end -module GHI - module Commands - class Edit < Command - attr_accessor :editor - - def options - OptionParser.new do |opts| - opts.banner = < -EOF - opts.separator '' - opts.on( - '-m', '--message []', 'change issue description' - ) do |text| - next self.editor = true if text.nil? - assigns[:title], assigns[:body] = text.split(/\n+/, 2) - end - opts.on( - '-u', '--[no-]assign []', 'assign to specified user' - ) do |assignee| - assigns[:assignee] = assignee - end - opts.on '--claim', 'assign to yourself' do - assigns[:assignee] = Authorization.username - end - opts.on( - '-s', '--state ', %w(open closed), - {'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'" - ) do |state| - assigns[:state] = state - end - opts.on( - '-M', '--[no-]milestone []', Integer, 'associate with milestone' - ) do |milestone| - assigns[:milestone] = milestone - end - opts.on( - '-L', '--label ...', Array, 'associate with label(s)' - ) do |labels| - (assigns[:labels] ||= []).concat labels - end - opts.separator '' - opts.separator 'Pull request options' - opts.on( - '-H', '--head [[:]]', - 'branch where your changes are implemented', - '(defaults to current branch)' - ) do |head| - self.action = 'pull' - assigns[:head] = head - end - opts.on( - '-b', '--base []', - 'branch you want your changes pulled into', '(defaults to master)' - ) do |base| - self.action = 'pull' - assigns[:base] = base - end - opts.separator '' - end - end - - def execute - self.action = 'edit' - require_repo - require_issue - options.parse! args - case action - when 'edit' - begin - if editor || assigns.empty? - i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body - e = Editor.new "GHI_ISSUE_#{issue}" - message = e.gets format_editor(i) - e.unlink "There's no issue." if message.nil? || message.empty? - assigns[:title], assigns[:body] = message.split(/\n+/, 2) - end - if i && assigns.keys.sort == [:body, :title] - titles_match = assigns[:title].strip == i['title'].strip - if assigns[:body] - bodies_match = assigns[:body].to_s.strip == i['body'].to_s.strip - end - if titles_match && bodies_match - e.unlink if e - abort 'No change.' if assigns.dup.delete_if { |k, v| - [:title, :body].include? k - } - end - end - unless assigns.empty? - i = throb { - api.patch "/repos/#{repo}/issues/#{issue}", assigns - }.body - puts format_issue(i) - puts 'Updated.' - end - e.unlink if e - rescue Client::Error => e - raise unless error = e.errors.first - abort "%s %s %s %s." % [ - error['resource'], - error['field'], - [*error['value']].join(', '), - error['code'] - ] - end - when 'pull' - begin - assigns[:issue] = issue - assigns[:base] ||= 'master' - head = begin - if ref = %x{ - git rev-parse --abbrev-ref HEAD@{upstream} 2>/dev/null - }.chomp! - ref.split('/').last if $? == 0 - end - end - assigns[:head] ||= head - if assigns[:head] - assigns[:head].sub!(/:$/, ":#{head}") - else - abort < e - raise unless error = e.errors.last - abort error['message'].sub(/^base /, '') - end - end - end - end - end -end -module GHI - module Commands - class Help < Command - def self.execute args, message = nil - new(args).execute message - end - - attr_accessor :command - - def options - OptionParser.new do |opts| - opts.banner = 'usage: ghi help [--all] [--man|--web] ' - opts.separator '' - opts.on('-a', '--all', 'print all available commands') { all } - opts.on('-m', '--man', 'show man page') { man } - opts.on('-w', '--web', 'show manual in web browser') { web } - opts.separator '' - end - end - - def execute message = nil - self.command = args.shift if args.first !~ /^-/ - - if command.nil? && args.empty? - puts message if message - puts <' for more information on a specific command. -EOF - exit - end - - options.parse! args.empty? ? %w(-m) : args - end - - def all - raise 'TODO' - end - - def man - GHI.execute [command, '-h'] - # TODO: - # exec "man #{['ghi', command].compact.join '-'}" - end - - def web - raise 'TODO' - end - end - end -end -module GHI - module Commands - class Label < Command - attr_accessor :name - - #-- - # FIXME: This does too much. Opt for a secondary command, e.g., - # - # ghi label add - # ghi label rm - # ghi label ... - #++ - def options - OptionParser.new do |opts| - opts.banner = < [-c ] [-r ] - or: ghi label -D - or: ghi label [-a] [-d] [-f] - or: ghi label -l [] -EOF - opts.separator '' - opts.on '-l', '--list []', 'list label names' do |n| - self.action = 'index' - @issue ||= n - end - opts.on '-D', '--delete', 'delete label' do - self.action = 'destroy' - end - opts.separator '' - opts.separator 'Label modification options' - opts.on( - '-c', '--color ', 'color name or 6-character hex code' - ) do |color| - assigns[:color] = to_hex color - self.action ||= 'create' - end - opts.on '-r', '--rename ', 'new label name' do |name| - assigns[:name] = name - self.action = 'update' - end - opts.separator '' - opts.separator 'Issue modification options' - opts.on '-a', '--add', 'add labels to issue' do - self.action = issue ? 'add' : 'create' - end - opts.on '-d', '--delete', 'remove labels from issue' do - self.action = issue ? 'remove' : 'destroy' - end - opts.on '-f', '--force', 'replace existing labels' do - self.action = issue ? 'replace' : 'update' - end - opts.separator '' - end - end - - def execute - extract_issue - require_repo - options.parse! args.empty? ? %w(-l) : args - - if issue - self.action ||= 'add' - self.name = args.shift.to_s.split ',' - self.name.concat args - else - self.action ||= 'create' - self.name ||= args.shift - end - - send action - end - - protected - - def index - if issue - uri = "/repos/#{repo}/issues/#{issue}/labels" - else - uri = "/repos/#{repo}/labels" - end - labels = throb { api.get uri }.body - if labels.empty? - puts 'None.' - else - puts labels.map { |label| - name = label['name'] - colorize? ? bg(label['color']) { " #{name} " } : name - } - end - end - - def create - label = throb { - api.post "/repos/#{repo}/labels", assigns.merge(:name => name) - }.body - return update if label.nil? - puts "%s created." % bg(label['color']) { " #{label['name']} "} - rescue Client::Error => e - if e.errors.find { |error| error['code'] == 'already_exists' } - return update - end - raise - end - - def update - label = throb { - api.patch "/repos/#{repo}/labels/#{name}", assigns - }.body - puts "%s updated." % bg(label['color']) { " #{label['name']} "} - end - - def destroy - throb { api.delete "/repos/#{repo}/labels/#{name}" } - puts "[#{name}] deleted." - end - - def add - labels = throb { - api.post "/repos/#{repo}/issues/#{issue}/labels", name - }.body - puts "Issue #%d labeled %s." % [issue, format_labels(labels)] - end - - def remove - case name.length - when 0 - throb { api.delete base_uri } - puts "Labels removed." - when 1 - labels = throb { api.delete "#{base_uri}/#{name.join}" }.body - if labels.empty? - puts "Issue #%d unlabeled." % issue - else - puts "Issue #%d labeled %s." % [issue, format_labels(labels)] - end - else - labels = throb { - api.get "/repos/#{repo}/issues/#{issue}/labels" - }.body - self.name = labels.map { |l| l['name'] } - name - replace - end - end - - def replace - labels = throb { api.put base_uri, name }.body - if labels.empty? - puts "Issue #%d unlabeled." % issue - else - puts "Issue #%d labeled %s." % [issue, format_labels(labels)] - end - end - - private - - def base_uri - "/repos/#{repo}/#{issue ? "issues/#{issue}/labels" : 'labels'}" - end - end - end -end -require 'date' - -module GHI - module Commands - class List < Command - attr_accessor :web - attr_accessor :reverse - attr_accessor :quiet - - def options - OptionParser.new do |opts| - opts.banner = 'usage: ghi list [options]' - opts.separator '' - opts.on '-a', '--global', '--all', 'all of your issues on GitHub' do - @repo = nil - end - opts.on( - '-s', '--state ', %w(open closed), - {'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'" - ) do |state| - assigns[:state] = state - end - opts.on( - '-L', '--label ...', Array, 'by label(s)' - ) do |labels| - (assigns[:labels] ||= []).concat labels - end - opts.on( - '-S', '--sort ', %w(created updated comments), - {'c'=>'created','u'=>'updated','m'=>'comments'}, - "'created', 'updated', or 'comments'" - ) do |sort| - assigns[:sort] = sort - end - opts.on '--reverse', 'reverse (ascending) sort order' do - self.reverse = !reverse - end - opts.on( - '--since ', 'issues more recent than', - "e.g., '2011-04-30'" - ) do |date| - begin - assigns[:since] = DateTime.parse date # TODO: Better parsing. - rescue ArgumentError => e - raise OptionParser::InvalidArgument, e.message - end - end - opts.on('-v', '--verbose') { self.verbose = true } - opts.on('-w', '--web') { self.web = true } - opts.separator '' - opts.separator 'Global options' - opts.on( - '-f', '--filter ', - filters = %w(assigned created mentioned subscribed), - Hash[filters.map { |f| [f[0, 1], f] }], - "'assigned', 'created', 'mentioned', or", "'subscribed'" - ) do |filter| - assigns[:filter] = filter - end - opts.separator '' - opts.separator 'Project options' - opts.on( - '-M', '--[no-]milestone []', Integer, - 'with (specified) milestone' - ) do |milestone| - assigns[:milestone] = any_or_none_or milestone - end - opts.on( - '-u', '--[no-]assignee []', 'assigned to specified user' - ) do |assignee| - assignee = assignee.sub /^@/, '' - assigns[:assignee] = any_or_none_or assignee - end - opts.on '--mine', 'assigned to you' do - assigns[:assignee] = Authorization.username - end - opts.on( - '-U', '--mentioned []', 'mentioning you or specified user' - ) do |mentioned| - assigns[:mentioned] = mentioned || Authorization.username - end - opts.separator '' - end - end - - def execute - if index = args.index { |arg| /^@/ === arg } - assigns[:assignee] = args.delete_at(index)[1..-1] - end - - begin - options.parse! args - rescue OptionParser::InvalidOption => e - fallback.parse! e.args - retry - end - assigns[:labels] = assigns[:labels].join ',' if assigns[:labels] - if reverse - assigns[:sort] ||= 'created' - assigns[:direction] = 'asc' - end - if web - Web.new(repo || 'dashboard').open 'issues', assigns - else - assigns[:per_page] = 100 - unless quiet - print header = format_issues_header - print "\n" unless paginate? - end - res = throb( - 0, format_state(assigns[:state], quiet ? CURSOR[:up][1] : '#') - ) { api.get uri, assigns } - print "\r#{CURSOR[:up][1]}" if header && paginate? - page header do - issues = res.body - if verbose - puts issues.map { |i| format_issue i } - else - puts format_issues(issues, repo.nil?) - end - break unless res.next_page - res = throb { api.get res.next_page } - end - end - rescue Client::Error => e - if e.response.code == '422' - e.errors.any? { |err| - err['code'] == 'missing' && err['field'] == 'milestone' - } and abort 'No such milestone.' - end - - raise - end - - private - - def uri - (repo ? "/repos/#{repo}" : '') << '/issues' - end - - def fallback - OptionParser.new do |opts| - opts.on('-c', '--closed') { assigns[:state] = 'closed' } - opts.on('-q', '--quiet') { self.quiet = true } - end - end - end - end -end -require 'date' - -module GHI - module Commands - class Milestone < Command - attr_accessor :edit - attr_accessor :reverse - attr_accessor :web - - #-- - # FIXME: Opt for better interface, e.g., - # - # ghi milestone [-v | --verbose] [--[no-]closed] - # ghi milestone add - # ghi milestone rm - #++ - def options - OptionParser.new do |opts| - opts.banner = <] [] - or: ghi milestone -D - or: ghi milestone -l [-c] [-v] -EOF - opts.separator '' - opts.on '-l', '--list', 'list milestones' do - self.action = 'index' - end - opts.on '-c', '--[no-]closed', 'show closed milestones' do |closed| - assigns[:state] = closed ? 'closed' : 'open' - end - opts.on( - '-S', '--sort ', %w(due_date completeness), - {'d'=>'due_date', 'due'=>'due_date', 'c'=>'completeness'}, - "'due_date' or 'completeness'" - ) do |sort| - assigns[:sort] = sort - end - opts.on '--reverse', 'reverse (ascending) sort order' do - self.reverse = !reverse - end - opts.on '-v', '--verbose', 'list milestones verbosely' do - self.verbose = true - end - opts.on('-w', '--web') { self.web = true } - opts.separator '' - opts.separator 'Milestone modification options' - opts.on( - '-m', '--message []', 'change milestone description' - ) do |text| - self.action = 'create' - self.edit = true - next unless text - assigns[:title], assigns[:description] = text.split(/\n+/, 2) - end - # FIXME: We already describe --[no-]closed; describe this, too? - opts.on( - '-s', '--state ', %w(open closed), - {'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'" - ) do |state| - self.action = 'create' - assigns[:state] = state - end - opts.on( - '--due ', 'when milestone should be complete', - "e.g., '2012-04-30'" - ) do |date| - self.action = 'create' - begin - # TODO: Better parsing. - assigns[:due_on] = DateTime.parse(date).strftime - rescue ArgumentError => e - raise OptionParser::InvalidArgument, e.message - end - end - opts.on '-D', '--delete', 'delete milestone' do - self.action = 'destroy' - end - opts.separator '' - end - end - - def execute - self.action = 'index' - require_repo - extract_milestone - - begin - options.parse! args - rescue OptionParser::AmbiguousOption => e - fallback.parse! e.args - end - - milestone and case action - when 'create' then self.action = 'update' - when 'index' then self.action = 'show' - end - - if reverse - assigns[:sort] ||= 'created' - assigns[:direction] = 'asc' - end - - case action - when 'index' - if web - Web.new(repo).open 'issues/milestones', assigns - else - assigns[:per_page] = 100 - state = assigns[:state] || 'open' - print format_state state, "# #{repo} #{state} milestones" - print "\n" unless paginate? - res = throb(0, format_state(state, '#')) { api.get uri, assigns } - page do - milestones = res.body - if verbose - puts milestones.map { |m| format_milestone m } - else - puts format_milestones(milestones) - end - break unless res.next_page - res = throb { api.get res.next_page } - end - end - when 'show' - if web - List.execute %W(-w -M #{milestone} -- #{repo}) - else - m = throb { api.get uri }.body - page do - puts format_milestone(m) - puts 'Issues:' - args.unshift(*%W(-q -M #{milestone} -- #{repo})) - args.unshift '-v' if verbose - List.execute args - break - end - end - when 'create' - if web - Web.new(repo).open 'issues/milestones/new' - else - if assigns[:title].nil? - e = Editor.new 'GHI_MILESTONE' - message = e.gets format_milestone_editor - e.unlink 'Empty milestone.' if message.nil? || message.empty? - assigns[:title], assigns[:description] = message.split(/\n+/, 2) - end - m = throb { api.post uri, assigns }.body - puts 'Milestone #%d created.' % m['number'] - e.unlink if e - end - when 'update' - if web - Web.new(repo).open "issues/milestones/#{milestone}/edit" - else - if edit || assigns.empty? - m = throb { api.get "/repos/#{repo}/milestones/#{milestone}" }.body - e = Editor.new "GHI_MILESTONE_#{milestone}" - message = e.gets format_milestone_editor(m) - e.unlink 'Empty milestone.' if message.nil? || message.empty? - assigns[:title], assigns[:description] = message.split(/\n+/, 2) - end - if assigns[:title] && m - t_match = assigns[:title].strip == m['title'].strip - if assigns[:description] - b_match = assigns[:description].strip == m['description'].strip - end - if t_match && b_match - e.unlink if e - abort 'No change.' if assigns.dup.delete_if { |k, v| - [:title, :description].include? k - } - end - end - m = throb { api.patch uri, assigns }.body - puts format_milestone(m) - puts 'Updated.' - e.unlink if e - end - when 'destroy' - require_milestone - throb { api.delete uri } - puts 'Milestone deleted.' - end - end - - private - - def uri - if milestone - "/repos/#{repo}/milestones/#{milestone}" - else - "/repos/#{repo}/milestones" - end - end - - def fallback - OptionParser.new do |opts| - opts.on '-d' do - self.action = 'destroy' - end - end - end - end - end -end -module GHI - module Commands - class Open < Command - attr_accessor :editor - attr_accessor :web - - def options - OptionParser.new do |opts| - opts.banner = < -EOF - opts.separator '' - opts.on '-l', '--list', 'list open tickets' do - self.action = 'index' - end - opts.on('-w', '--web') { self.web = true } - opts.separator '' - opts.separator 'Issue modification options' - opts.on '-m', '--message []', 'describe issue' do |text| - if text - assigns[:title], assigns[:body] = text.split(/\n+/, 2) - else - self.editor = true - end - end - opts.on( - '-u', '--[no-]assign []', 'assign to specified user' - ) do |assignee| - assigns[:assignee] = assignee - end - opts.on '--claim', 'assign to yourself' do - assigns[:assignee] = Authorization.username - end - opts.on( - '-M', '--milestone ', 'associate with milestone' - ) do |milestone| - assigns[:milestone] = milestone - end - opts.on( - '-L', '--label ...', Array, 'associate with label(s)' - ) do |labels| - (assigns[:labels] ||= []).concat labels - end - opts.separator '' - end - end - - def execute - require_repo - self.action = 'create' - - if extract_issue - Edit.execute args.push('-so', issue, '--', repo) - exit - end - - options.parse! args - - case action - when 'index' - if assigns.key? :assignee - args.unshift assigns[:assignee] if assigns[:assignee] - args.unshift '-u' - end - args.unshift '-w' if web - List.execute args.push('--', repo) - when 'create' - if web - Web.new(repo).open 'issues/new' - else - unless args.empty? - assigns[:title], assigns[:body] = args.join(' '), assigns[:title] - end - assigns[:title] = args.join ' ' unless args.empty? - if assigns[:title].nil? || editor - e = Editor.new 'GHI_ISSUE' - message = e.gets format_editor(assigns) - e.unlink "There's no issue?" if message.nil? || message.empty? - assigns[:title], assigns[:body] = message.split(/\n+/, 2) - end - i = throb { api.post "/repos/#{repo}/issues", assigns }.body - e.unlink if e - puts format_issue(i) - puts 'Opened.' - end - end - rescue Client::Error => e - raise unless error = e.errors.first - abort "%s %s %s %s." % [ - error['resource'], - error['field'], - [*error['value']].join(', '), - error['code'] - ] - end - end - end -end -module GHI - module Commands - class Show < Command - attr_accessor :web - - def options - OptionParser.new do |opts| - opts.banner = 'usage: ghi show ' - opts.separator '' - opts.on('-w', '--web') { self.web = true } - end - end - - def execute - require_issue - require_repo - options.parse! args - if web - Web.new(repo).open "issues/#{issue}" - else - i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body - page do - puts format_issue(i) - n = i['comments'] - if n > 0 - puts "#{n} comment#{'s' unless n == 1}:\n\n" - Comment.execute %W(-l #{issue} -- #{repo}) - end - break - end - end - end - end - end -end -module GHI - module Commands - module Version - MAJOR = 0 - MINOR = 9 - PATCH = 0 - PRE = 20120731 - - VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join '.' - - def self.execute args - puts "ghi version #{VERSION}" - end - end - end -end -#!/usr/bin/env ruby -GHI.execute ARGV diff --git a/bin/hub b/bin/hub deleted file mode 100755 index 1d96b80..0000000 --- a/bin/hub +++ /dev/null @@ -1,2625 +0,0 @@ -#!/usr/bin/env ruby -# -# This file is generated code. DO NOT send patches for it. -# -# Original source files with comments are at: -# https://github.com/defunkt/hub -# - -module Hub - Version = VERSION = '1.10.6' -end - -module Hub - class Args < Array - attr_accessor :executable - - def initialize(*args) - super - @executable = ENV["GIT"] || "git" - @skip = @noop = false - @original_args = args.first - @chain = [nil] - end - - def after(cmd_or_args = nil, args = nil, &block) - @chain.insert(-1, normalize_callback(cmd_or_args, args, block)) - end - - def before(cmd_or_args = nil, args = nil, &block) - @chain.insert(@chain.index(nil), normalize_callback(cmd_or_args, args, block)) - end - - def chained? - @chain.size > 1 - end - - def commands - chain = @chain.dup - chain[chain.index(nil)] = self.to_exec - chain - end - - def skip! - @skip = true - end - - def skip? - @skip - end - - def noop! - @noop = true - end - - def noop? - @noop - end - - def to_exec(args = self) - Array(executable) + args - end - - def add_exec_flags(flags) - self.executable = Array(executable).concat(flags) - end - - def words - reject { |arg| arg.index('-') == 0 } - end - - def flags - self - words - end - - def changed? - chained? or self != @original_args - end - - def has_flag?(*flags) - pattern = flags.flatten.map { |f| Regexp.escape(f) }.join('|') - !grep(/^#{pattern}(?:=|$)/).empty? - end - - private - - def normalize_callback(cmd_or_args, args, block) - if block - block - elsif args - [cmd_or_args].concat args - elsif Array === cmd_or_args - self.to_exec cmd_or_args - elsif cmd_or_args - cmd_or_args - else - raise ArgumentError, "command or block required" - end - end - end -end - -module Hub - class SshConfig - CONFIG_FILES = %w(~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config) - - def initialize files = nil - @settings = Hash.new {|h,k| h[k] = {} } - Array(files || CONFIG_FILES).each do |path| - file = File.expand_path path - parse_file file if File.exist? file - end - end - - def get_value hostname, key - key = key.to_s.downcase - @settings.each do |pattern, settings| - if pattern.match? hostname and found = settings[key] - return found - end - end - yield - end - - class HostPattern - def initialize pattern - @pattern = pattern.to_s.downcase - end - - def to_s() @pattern end - def ==(other) other.to_s == self.to_s end - - def matcher - @matcher ||= - if '*' == @pattern - Proc.new { true } - elsif @pattern !~ /[?*]/ - lambda { |hostname| hostname.to_s.downcase == @pattern } - else - re = self.class.pattern_to_regexp @pattern - lambda { |hostname| re =~ hostname } - end - end - - def match? hostname - matcher.call hostname - end - - def self.pattern_to_regexp pattern - escaped = Regexp.escape(pattern) - escaped.gsub!('\*', '.*') - escaped.gsub!('\?', '.') - /^#{escaped}$/i - end - end - - def parse_file file - host_patterns = [HostPattern.new('*')] - - IO.foreach(file) do |line| - case line - when /^\s*(#|$)/ then next - when /^\s*(\S+)\s*=/ - key, value = $1, $' - else - key, value = line.strip.split(/\s+/, 2) - end - - next if value.nil? - key.downcase! - value = $1 if value =~ /^"(.*)"$/ - value.chomp! - - if 'host' == key - host_patterns = value.split(/\s+/).map {|p| HostPattern.new p } - else - record_setting key, value, host_patterns - end - end - end - - def record_setting key, value, patterns - patterns.each do |pattern| - @settings[pattern][key] ||= value - end - end - end -end - -require 'uri' -require 'yaml' -require 'forwardable' -require 'fileutils' - -module Hub - class GitHubAPI - attr_reader :config, :oauth_app_url - - def initialize config, options - @config = config - @oauth_app_url = options.fetch(:app_url) - end - - module Exceptions - def self.===(exception) - exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions' - end - end - - def api_host host - host = host.downcase - 'github.com' == host ? 'api.github.com' : host - end - - def repo_info project - get "https://%s/repos/%s/%s" % - [api_host(project.host), project.owner, project.name] - end - - def repo_exists? project - repo_info(project).success? - end - - def fork_repo project - res = post "https://%s/repos/%s/%s/forks" % - [api_host(project.host), project.owner, project.name] - res.error! unless res.success? - end - - def create_repo project, options = {} - is_org = project.owner.downcase != config.username(api_host(project.host)).downcase - params = { :name => project.name, :private => !!options[:private] } - params[:description] = options[:description] if options[:description] - params[:homepage] = options[:homepage] if options[:homepage] - - if is_org - res = post "https://%s/orgs/%s/repos" % [api_host(project.host), project.owner], params - else - res = post "https://%s/user/repos" % api_host(project.host), params - end - res.error! unless res.success? - res.data - end - - def pullrequest_info project, pull_id - res = get "https://%s/repos/%s/%s/pulls/%d" % - [api_host(project.host), project.owner, project.name, pull_id] - res.error! unless res.success? - res.data - end - - def create_pullrequest options - project = options.fetch(:project) - params = { - :base => options.fetch(:base), - :head => options.fetch(:head) - } - - if options[:issue] - params[:issue] = options[:issue] - else - params[:title] = options[:title] if options[:title] - params[:body] = options[:body] if options[:body] - end - - res = post "https://%s/repos/%s/%s/pulls" % - [api_host(project.host), project.owner, project.name], params - - res.error! unless res.success? - res.data - end - - def statuses project, sha - res = get "https://%s/repos/%s/%s/statuses/%s" % - [api_host(project.host), project.owner, project.name, sha] - - res.error! unless res.success? - res.data - end - - module HttpMethods - module ResponseMethods - def status() code.to_i end - def data?() content_type =~ /\bjson\b/ end - def data() @data ||= JSON.parse(body) end - def error_message?() data? and data['errors'] || data['message'] end - def error_message() error_sentences || data['message'] end - def success?() Net::HTTPSuccess === self end - def error_sentences - data['errors'].map do |err| - case err['code'] - when 'custom' then err['message'] - when 'missing_field' - %(Missing field: "%s") % err['field'] - when 'invalid' - %(Invalid value for "%s": "%s") % [ err['field'], err['value'] ] - when 'unauthorized' - %(Not allowed to change field "%s") % err['field'] - end - end.compact if data['errors'] - end - end - - def get url, &block - perform_request url, :Get, &block - end - - def post url, params = nil - perform_request url, :Post do |req| - if params - req.body = JSON.dump params - req['Content-Type'] = 'application/json;charset=utf-8' - end - yield req if block_given? - req['Content-Length'] = byte_size req.body - end - end - - def byte_size str - if str.respond_to? :bytesize then str.bytesize - elsif str.respond_to? :length then str.length - else 0 - end - end - - def post_form url, params - post(url) {|req| req.set_form_data params } - end - - def perform_request url, type - url = URI.parse url unless url.respond_to? :host - - require 'net/https' - req = Net::HTTP.const_get(type).new request_uri(url) - http = configure_connection(req, url) do |host_url| - create_connection host_url - end - - req['User-Agent'] = "Hub #{Hub::VERSION}" - apply_authentication(req, url) - yield req if block_given? - - begin - res = http.start { http.request(req) } - res.extend ResponseMethods - return res - rescue SocketError => err - raise Context::FatalError, "error with #{type.to_s.upcase} #{url} (#{err.message})" - end - end - - def request_uri url - str = url.request_uri - str = '/api/v3' << str if url.host != 'api.github.com' - str - end - - def configure_connection req, url - if ENV['HUB_TEST_HOST'] - req['Host'] = url.host - url = url.dup - url.scheme = 'http' - url.host, test_port = ENV['HUB_TEST_HOST'].split(':') - url.port = test_port.to_i if test_port - end - yield url - end - - def apply_authentication req, url - user = url.user || config.username(url.host) - pass = config.password(url.host, user) - req.basic_auth user, pass - end - - def create_connection url - use_ssl = 'https' == url.scheme - - proxy_args = [] - if proxy = config.proxy_uri(use_ssl) - proxy_args << proxy.host << proxy.port - if proxy.userinfo - require 'cgi' - proxy_args.concat proxy.userinfo.split(':', 2).map {|a| CGI.unescape a } - end - end - - http = Net::HTTP.new(url.host, url.port, *proxy_args) - - if http.use_ssl = use_ssl - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - return http - end - end - - module OAuth - def apply_authentication req, url - if (req.path =~ /\/authorizations$/) - super - else - refresh = false - user = url.user || config.username(url.host) - token = config.oauth_token(url.host, user) { - refresh = true - obtain_oauth_token url.host, user - } - if refresh - res = get "https://#{url.host}/user" - res.error! unless res.success? - config.update_username(url.host, user, res.data['login']) - end - req['Authorization'] = "token #{token}" - end - end - - def obtain_oauth_token host, user - res = get "https://#{user}@#{host}/authorizations" - res.error! unless res.success? - - if found = res.data.find {|auth| auth['app']['url'] == oauth_app_url } - found['token'] - else - res = post "https://#{user}@#{host}/authorizations", - :scopes => %w[repo], :note => 'hub', :note_url => oauth_app_url - res.error! unless res.success? - res.data['token'] - end - end - end - - include HttpMethods - include OAuth - - class FileStore - extend Forwardable - def_delegator :@data, :[], :get - def_delegator :@data, :[]=, :set - - def initialize filename - @filename = filename - @data = Hash.new {|d, host| d[host] = [] } - load if File.exist? filename - end - - def fetch_user host - unless entry = get(host).first - user = yield - return nil if user.nil? or user.empty? - entry = entry_for_user(host, user) - end - entry['user'] - end - - def fetch_value host, user, key - entry = entry_for_user host, user - entry[key.to_s] || begin - value = yield - if value and !value.empty? - entry[key.to_s] = value - save - value - else - raise "no value" - end - end - end - - def entry_for_user host, username - entries = get(host) - entries.find {|e| e['user'] == username } or - (entries << {'user' => username}).last - end - - def load - existing_data = File.read(@filename) - @data.update YAML.load(existing_data) unless existing_data.strip.empty? - end - - def save - FileUtils.mkdir_p File.dirname(@filename) - File.open(@filename, 'w', 0600) {|f| f << YAML.dump(@data) } - end - end - - class Configuration - def initialize store - @data = store - @password_cache = {} - end - - def normalize_host host - host = host.downcase - 'api.github.com' == host ? 'github.com' : host - end - - def username host - return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.empty? - host = normalize_host host - @data.fetch_user host do - if block_given? then yield - else prompt "#{host} username" - end - end - end - - def update_username host, old_username, new_username - entry = @data.entry_for_user(normalize_host(host), old_username) - entry['user'] = new_username - @data.save - end - - def api_token host, user - host = normalize_host host - @data.fetch_value host, user, :api_token do - if block_given? then yield - else prompt "#{host} API token for #{user}" - end - end - end - - def password host, user - return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.empty? - host = normalize_host host - @password_cache["#{user}@#{host}"] ||= prompt_password host, user - end - - def oauth_token host, user, &block - @data.fetch_value normalize_host(host), user, :oauth_token, &block - end - - def prompt what - print "#{what}: " - $stdin.gets.chomp - end - - def prompt_password host, user - print "#{host} password for #{user} (never stored): " - if $stdin.tty? - password = askpass - puts '' - password - else - $stdin.gets.chomp - end - end - - NULL = defined?(File::NULL) ? File::NULL : - File.exist?('/dev/null') ? '/dev/null' : 'NUL' - - def askpass - tty_state = `stty -g 2>#{NULL}` - system 'stty raw -echo -icanon isig' if $?.success? - pass = '' - while char = getbyte($stdin) and !(char == 13 or char == 10) - if char == 127 or char == 8 - pass[-1,1] = '' unless pass.empty? - else - pass << char.chr - end - end - pass - ensure - system "stty #{tty_state}" unless tty_state.empty? - end - - def getbyte(io) - if io.respond_to?(:getbyte) - io.getbyte - else - io.getc - end - end - - def proxy_uri(with_ssl) - env_name = "HTTP#{with_ssl ? 'S' : ''}_PROXY" - if proxy = ENV[env_name] || ENV[env_name.downcase] and !proxy.empty? - proxy = "http://#{proxy}" unless proxy.include? '://' - URI.parse proxy - end - end - end - end -end - -require 'shellwords' -require 'forwardable' -require 'uri' - -module Hub - module Context - extend Forwardable - - NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL' - - class GitReader - attr_reader :executable - - def initialize(executable = nil, &read_proc) - @executable = executable || 'git' - read_proc ||= lambda { |cache, cmd| - result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp - cache[cmd] = $?.success? && !result.empty? ? result : nil - } - @cache = Hash.new(&read_proc) - end - - def add_exec_flags(flags) - @executable = Array(executable).concat(flags) - end - - def read_config(cmd, all = false) - config_cmd = ['config', (all ? '--get-all' : '--get'), *cmd] - config_cmd = config_cmd.join(' ') unless cmd.respond_to? :join - read config_cmd - end - - def read(cmd) - @cache[cmd] - end - - def stub_config_value(key, value, get = '--get') - stub_command_output "config #{get} #{key}", value - end - - def stub_command_output(cmd, value) - @cache[cmd] = value.nil? ? nil : value.to_s - end - - def stub!(values) - @cache.update values - end - - private - - def to_exec(args) - args = Shellwords.shellwords(args) if args.respond_to? :to_str - Array(executable) + Array(args) - end - - def command_to_string(cmd) - full_cmd = to_exec(cmd) - full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ') - end - end - - module GitReaderMethods - extend Forwardable - - def_delegator :git_reader, :read_config, :git_config - def_delegator :git_reader, :read, :git_command - - def self.extended(base) - base.extend Forwardable - base.def_delegators :'self.class', :git_config, :git_command - end - end - - class Error < RuntimeError; end - class FatalError < Error; end - - private - - def git_reader - @git_reader ||= GitReader.new ENV['GIT'] - end - - include GitReaderMethods - private :git_config, :git_command - - def local_repo(fatal = true) - @local_repo ||= begin - if is_repo? - LocalRepo.new git_reader, current_dir - elsif fatal - raise FatalError, "Not a git repository" - end - end - end - - repo_methods = [ - :current_branch, - :current_project, :upstream_project, - :repo_owner, :repo_host, - :remotes, :remotes_group, :origin_remote - ] - def_delegator :local_repo, :name, :repo_name - def_delegators :local_repo, *repo_methods - private :repo_name, *repo_methods - - def master_branch - if local_repo(false) - local_repo.master_branch - else - Branch.new nil, 'refs/heads/master' - end - end - - class LocalRepo < Struct.new(:git_reader, :dir) - include GitReaderMethods - - def name - if project = main_project - project.name - else - File.basename(dir) - end - end - - def repo_owner - if project = main_project - project.owner - end - end - - def repo_host - project = main_project and project.host - end - - def main_project - remote = origin_remote and remote.project - end - - def upstream_project - if branch = current_branch and upstream = branch.upstream and upstream.remote? - remote = remote_by_name upstream.remote_name - remote.project - end - end - - def current_project - upstream_project || main_project - end - - def current_branch - if branch = git_command('symbolic-ref -q HEAD') - Branch.new self, branch - end - end - - def master_branch - Branch.new self, 'refs/heads/master' - end - - def remotes - @remotes ||= begin - list = git_command('remote').to_s.split("\n") - main = list.delete('origin') and list.unshift(main) - list.map { |name| Remote.new self, name } - end - end - - def remotes_group(name) - git_config "remotes.#{name}" - end - - def origin_remote - remotes.first - end - - def remote_by_name(remote_name) - remotes.find {|r| r.name == remote_name } - end - - def known_hosts - hosts = git_config('hub.host', :all).to_s.split("\n") - hosts << default_host - hosts << "ssh.#{default_host}" - end - - def self.default_host - ENV['GITHUB_HOST'] || main_host - end - - def self.main_host - 'github.com' - end - - extend Forwardable - def_delegators :'self.class', :default_host, :main_host - - def ssh_config - @ssh_config ||= SshConfig.new - end - end - - class GithubProject < Struct.new(:local_repo, :owner, :name, :host) - def self.from_url(url, local_repo) - if local_repo.known_hosts.include? url.host - _, owner, name = url.path.split('/', 4) - GithubProject.new(local_repo, owner, name.sub(/\.git$/, ''), url.host) - end - end - - attr_accessor :repo_data - - def initialize(*args) - super - self.name = self.name.tr(' ', '-') - self.host ||= (local_repo || LocalRepo).default_host - self.host = host.sub(/^ssh\./i, '') if 'ssh.github.com' == host.downcase - end - - def private? - repo_data ? repo_data.fetch('private') : - host != (local_repo || LocalRepo).main_host - end - - def owned_by(new_owner) - new_project = dup - new_project.owner = new_owner - new_project - end - - def name_with_owner - "#{owner}/#{name}" - end - - def ==(other) - name_with_owner == other.name_with_owner - end - - def remote - local_repo.remotes.find { |r| r.project == self } - end - - def web_url(path = nil) - project_name = name_with_owner - if project_name.sub!(/\.wiki$/, '') - unless '/wiki' == path - path = if path =~ %r{^/commits/} then '/_history' - else path.to_s.sub(/\w+/, '_\0') - end - path = '/wiki' + path - end - end - "https://#{host}/" + project_name + path.to_s - end - - def git_url(options = {}) - if options[:https] then "https://#{host}/" - elsif options[:private] or private? then "git@#{host}:" - else "git://#{host}/" - end + name_with_owner + '.git' - end - end - - class GithubURL < URI::HTTPS - extend Forwardable - - attr_reader :project - def_delegator :project, :name, :project_name - def_delegator :project, :owner, :project_owner - - def self.resolve(url, local_repo) - u = URI(url) - if %[http https].include? u.scheme and project = GithubProject.from_url(u, local_repo) - self.new(u.scheme, u.userinfo, u.host, u.port, u.registry, - u.path, u.opaque, u.query, u.fragment, project) - end - rescue URI::InvalidURIError - nil - end - - def initialize(*args) - @project = args.pop - super(*args) - end - - def project_path - path.split('/', 4)[3] - end - end - - class Branch < Struct.new(:local_repo, :name) - alias to_s name - - def short_name - name.sub(%r{^refs/(remotes/)?.+?/}, '') - end - - def master? - short_name == 'master' - end - - def upstream - if branch = local_repo.git_command("rev-parse --symbolic-full-name #{short_name}@{upstream}") - Branch.new local_repo, branch - end - end - - def remote? - name.index('refs/remotes/') == 0 - end - - def remote_name - name =~ %r{^refs/remotes/([^/]+)} and $1 or - raise Error, "can't get remote name from #{name.inspect}" - end - end - - class Remote < Struct.new(:local_repo, :name) - alias to_s name - - def ==(other) - other.respond_to?(:to_str) ? name == other.to_str : super - end - - def project - urls.each_value { |url| - if valid = GithubProject.from_url(url, local_repo) - return valid - end - } - nil - end - - def urls - return @urls if defined? @urls - @urls = {} - local_repo.git_command('remote -v').to_s.split("\n").map do |line| - next if line !~ /^(.+?)\t(.+) \((.+)\)$/ - remote, uri, type = $1, $2, $3 - next if remote != self.name - if uri =~ %r{^[\w-]+://} or uri =~ %r{^([^/]+?):} - uri = "ssh://#{$1}/#{$'}" if $1 - begin - @urls[type] = uri_parse(uri) - rescue URI::InvalidURIError - end - end - end - @urls - end - - def uri_parse uri - uri = URI.parse uri - uri.host = local_repo.ssh_config.get_value(uri.host, 'hostname') { uri.host } - uri.user = local_repo.ssh_config.get_value(uri.host, 'user') { uri.user } - uri - end - end - - - def github_project(name, owner = nil) - if owner and owner.index('/') - owner, name = owner.split('/', 2) - elsif name and name.index('/') - owner, name = name.split('/', 2) - else - name ||= repo_name - owner ||= github_user - end - - if local_repo(false) and main_project = local_repo.main_project - project = main_project.dup - project.owner = owner - project.name = name - project - else - GithubProject.new(local_repo(false), owner, name) - end - end - - def git_url(owner = nil, name = nil, options = {}) - project = github_project(name, owner) - project.git_url({:https => https_protocol?}.update(options)) - end - - def resolve_github_url(url) - GithubURL.resolve(url, local_repo) if url =~ /^https?:/ - end - - def http_clone? - git_config('--bool hub.http-clone') == 'true' - end - - def https_protocol? - git_config('hub.protocol') == 'https' or http_clone? - end - - def git_alias_for(name) - git_config "alias.#{name}" - end - - def rev_list(a, b) - git_command("rev-list --cherry-pick --right-only --no-merges #{a}...#{b}") - end - - PWD = Dir.pwd - - def current_dir - PWD - end - - def git_dir - git_command 'rev-parse -q --git-dir' - end - - def is_repo? - !!git_dir - end - - def git_editor - editor = git_command 'var GIT_EDITOR' - editor = ENV[$1] if editor =~ /^\$(\w+)$/ - editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/ - if File.exist? editor then [editor] - else editor.shellsplit - end - end - - module System - def browser_launcher - browser = ENV['BROWSER'] || ( - osx? ? 'open' : windows? ? %w[cmd /c start] : - %w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm } - ) - - abort "Please set $BROWSER to a web launcher to use this command." unless browser - Array(browser) - end - - def osx? - require 'rbconfig' - RbConfig::CONFIG['host_os'].to_s.include?('darwin') - end - - def windows? - require 'rbconfig' - RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ - end - - def which(cmd) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| - exts.each { |ext| - exe = "#{path}/#{cmd}#{ext}" - return exe if File.executable? exe - } - end - return nil - end - - def command?(name) - !which(name).nil? - end - - def tmp_dir - ENV['TMPDIR'] || ENV['TEMP'] || '/tmp' - end - end - - include System - extend System - end -end - -require 'strscan' -require 'forwardable' - -class Hub::JSON - def self.parse(data) new(data).parse end - - WSP = /\s+/ - OBJ = /[{\[]/; HEN = /\}/; AEN = /\]/ - COL = /\s*:\s*/; KEY = /\s*,\s*/ - NUM = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/ - BOL = /true|false/; NUL = /null/ - - extend Forwardable - - attr_reader :scanner - alias_method :s, :scanner - def_delegators :scanner, :scan, :matched - private :s, :scan, :matched - - def initialize data - @scanner = StringScanner.new data.to_s - end - - def parse - space - object - end - - private - - def space() scan WSP end - - def endkey() scan(KEY) or space end - - def object - matched == '{' ? hash : array if scan(OBJ) - end - - def value - object or string or - scan(NUL) ? nil : - scan(BOL) ? matched.size == 4: - scan(NUM) ? eval(matched) : - error - end - - def hash - obj = {} - space - repeat_until(HEN) { k = string; scan(COL); obj[k] = value; endkey } - obj - end - - def array - ary = [] - space - repeat_until(AEN) { ary << value; endkey } - ary - end - - SPEC = {'b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t"} - UNI = 'u'; CODE = /[a-fA-F0-9]{4}/ - STR = /"/; STE = '"' - ESC = '\\' - - def string - if scan(STR) - str, esc = '', false - while c = s.getch - if esc - str << (c == UNI ? (s.scan(CODE) || error).to_i(16).chr : SPEC[c] || c) - esc = false - else - case c - when ESC then esc = true - when STE then break - else str << c - end - end - end - str - end - end - - def error - raise "parse error at: #{scan(/.{1,10}/m).inspect}" - end - - def repeat_until reg - until scan(reg) - pos = s.pos - yield - error unless s.pos > pos - end - end - - module Generator - def generate(obj) - raise ArgumentError unless obj.is_a? Array or obj.is_a? Hash - generate_type(obj) - end - alias dump generate - - private - - def generate_type(obj) - type = obj.is_a?(Numeric) ? :Numeric : obj.class.name - begin send(:"generate_#{type}", obj) - rescue NoMethodError; raise ArgumentError, "can't serialize #{type}" - end - end - - ESC_MAP = Hash.new {|h,k| k }.update \ - "\r" => 'r', - "\n" => 'n', - "\f" => 'f', - "\t" => 't', - "\b" => 'b' - - def generate_String(str) - escaped = str.gsub(/[\r\n\f\t\b"\\]/) { "\\#{ESC_MAP[$&]}"} - %("#{escaped}") - end - - def generate_simple(obj) obj.inspect end - alias generate_Numeric generate_simple - alias generate_TrueClass generate_simple - alias generate_FalseClass generate_simple - - def generate_Symbol(sym) generate_String(sym.to_s) end - - def generate_NilClass(*) 'null' end - - def generate_Array(ary) '[%s]' % ary.map {|o| generate_type(o) }.join(', ') end - - def generate_Hash(hash) - '{%s}' % hash.map { |key, value| - "#{generate_String(key.to_s)}: #{generate_type(value)}" - }.join(', ') - end - end - - extend Generator -end - -module Hub - module Commands - instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ } - extend self - - extend Context - - NAME_RE = /[\w.][\w.-]*/ - OWNER_RE = /[a-zA-Z0-9][a-zA-Z0-9-]*/ - NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/ - - CUSTOM_COMMANDS = %w[alias create browse compare fork pull-request ci-status] - - def run(args) - slurp_global_flags(args) - - args.unshift 'help' if args.empty? - - cmd = args[0] - if expanded_args = expand_alias(cmd) - cmd = expanded_args[0] - expanded_args.concat args[1..-1] - end - - respect_help_flags(expanded_args || args) if custom_command? cmd - - cmd = cmd.gsub(/(\w)-/, '\1_') - if method_defined?(cmd) and cmd != 'run' - args.replace expanded_args if expanded_args - send(cmd, args) - end - rescue Errno::ENOENT - if $!.message.include? "No such file or directory - git" - abort "Error: `git` command not found" - else - raise - end - rescue Context::FatalError => err - abort "fatal: #{err.message}" - end - - - def ci_status(args) - args.shift - ref = args.words.first || 'HEAD' - - unless head_project = local_repo.current_project - abort "Aborted: the origin remote doesn't point to a GitHub repository." - end - - unless sha = local_repo.git_command("rev-parse -q #{ref}") - abort "Aborted: no revision could be determined from '#{ref}'" - end - - statuses = api_client.statuses(head_project, sha) - status = statuses.first - ref_state = status ? status['state'] : 'no status' - - exit_code = case ref_state - when 'success' then 0 - when 'failure', 'error' then 1 - when 'pending' then 2 - else 3 - end - - $stdout.puts ref_state - exit exit_code - end - - def pull_request(args) - args.shift - options = { } - force = explicit_owner = false - base_project = local_repo.main_project - head_project = local_repo.current_project - - unless current_branch - abort "Aborted: not currently on any branch." - end - - unless base_project - abort "Aborted: the origin remote doesn't point to a GitHub repository." - end - - from_github_ref = lambda do |ref, context_project| - if ref.index(':') - owner, ref = ref.split(':', 2) - project = github_project(context_project.name, owner) - end - [project || context_project, ref] - end - - while arg = args.shift - case arg - when '-f' - force = true - when '-F', '--file' - file = args.shift - text = file == '-' ? $stdin.read : File.read(file) - options[:title], options[:body] = read_msg(text) - when '-m', '--message' - text = args.shift - options[:title], options[:body] = read_msg(text) - when '-b' - base_project, options[:base] = from_github_ref.call(args.shift, base_project) - when '-h' - head = args.shift - explicit_owner = !!head.index(':') - head_project, options[:head] = from_github_ref.call(head, head_project) - when '-i' - options[:issue] = args.shift - else - if url = resolve_github_url(arg) and url.project_path =~ /^issues\/(\d+)/ - options[:issue] = $1 - base_project = url.project - elsif !options[:title] - options[:title] = arg - warn "hub: Specifying pull request title without a flag is deprecated." - warn "Please use one of `-m' or `-F' options." - else - abort "invalid argument: #{arg}" - end - end - end - - options[:project] = base_project - options[:base] ||= master_branch.short_name - - if tracked_branch = options[:head].nil? && current_branch.upstream - if !tracked_branch.remote? - tracked_branch = nil - elsif base_project == head_project and tracked_branch.short_name == options[:base] - $stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})" - warn "(use `-h ` to specify an explicit pull request head)" - abort - end - end - options[:head] ||= (tracked_branch || current_branch).short_name - - user = github_user(head_project.host) - if head_project.owner != user and !tracked_branch and !explicit_owner - head_project = head_project.owned_by(user) - end - - remote_branch = "#{head_project.remote}/#{options[:head]}" - options[:head] = "#{head_project.owner}:#{options[:head]}" - - if !force and tracked_branch and local_commits = rev_list(remote_branch, nil) - $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}" - warn "(use `-f` to force submit a pull request anyway)" - abort - end - - if args.noop? - puts "Would request a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}" - exit - end - - unless options[:title] or options[:issue] - base_branch = "#{base_project.remote}/#{options[:base]}" - commits = rev_list(base_branch, remote_branch).to_s.split("\n") - - case commits.size - when 0 - default_message = commit_summary = nil - when 1 - format = '%w(78,0,0)%s%n%+b' - default_message = git_command "show -s --format='#{format}' #{commits.first}" - commit_summary = nil - else - format = '%h (%aN, %ar)%n%w(78,3,3)%s%n%+b' - default_message = nil - commit_summary = git_command "log --no-color --format='%s' --cherry %s...%s" % - [format, base_branch, remote_branch] - end - - options[:title], options[:body] = pullrequest_editmsg(commit_summary) { |msg, initial_message| - initial_message ||= default_message - msg.puts initial_message if initial_message - msg.puts "" - msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}" - msg.puts "#" - msg.puts "# Write a message for this pull request. The first block" - msg.puts "# of text is the title and the rest is description." - } - end - - pull = api_client.create_pullrequest(options) - - args.executable = 'echo' - args.replace [pull['html_url']] - rescue GitHubAPI::Exceptions - response = $!.response - display_api_exception("creating pull request", response) - if 404 == response.status - base_url = base_project.web_url.split('://', 2).last - warn "Are you sure that #{base_url} exists?" - end - exit 1 - else - delete_editmsg - end - - def clone(args) - ssh = args.delete('-p') - has_values = /^(--(upload-pack|template|depth|origin|branch|reference|name)|-[ubo])$/ - - idx = 1 - while idx < args.length - arg = args[idx] - if arg.index('-') == 0 - idx += 1 if arg =~ has_values - else - if arg =~ NAME_WITH_OWNER_RE and !File.directory?(arg) - name, owner = arg, nil - owner, name = name.split('/', 2) if name.index('/') - project = github_project(name, owner || github_user) - ssh ||= args[0] != 'submodule' && project.owner == github_user(project.host) { } - args[idx] = project.git_url(:private => ssh, :https => https_protocol?) - end - break - end - idx += 1 - end - end - - def submodule(args) - return unless index = args.index('add') - args.delete_at index - - clone(args) - args.insert index, 'add' - end - - def remote(args) - if %w[add set-url].include?(args[1]) - name = args.last - if name =~ /^(#{OWNER_RE})$/ || name =~ /^(#{OWNER_RE})\/(#{NAME_RE})$/ - user, repo = $1, $2 || repo_name - end - end - return unless user # do not touch arguments - - ssh = args.delete('-p') - - if args.words[2] == 'origin' && args.words[3].nil? - user, repo = github_user, repo_name - elsif args.words[-2] == args.words[1] - idx = args.index( args.words[-1] ) - args[idx] = user - else - args.pop - end - - args << git_url(user, repo, :private => ssh) - end - - def fetch(args) - if args.include?('--multiple') - names = args.words[1..-1] - elsif remote_name = args.words[1] - if remote_name =~ /^\w+(,\w+)+$/ - index = args.index(remote_name) - args.delete(remote_name) - names = remote_name.split(',') - args.insert(index, *names) - args.insert(index, '--multiple') - else - names = [remote_name] - end - else - names = [] - end - - projects = names.map { |name| - unless name !~ /^#{OWNER_RE}$/ or remotes.include?(name) or remotes_group(name) - project = github_project(nil, name) - repo_info = api_client.repo_info(project) - if repo_info.success? - project.repo_data = repo_info.data - project - end - end - }.compact - - if projects.any? - projects.each do |project| - args.before ['remote', 'add', project.owner, project.git_url(:https => https_protocol?)] - end - end - end - - def checkout(args) - _, url_arg, new_branch_name = args.words - if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/ - pull_id = $1 - pull_data = api_client.pullrequest_info(url.project, pull_id) - - args.delete new_branch_name - user, branch = pull_data['head']['label'].split(':', 2) - abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo'] - new_branch_name ||= "#{user}-#{branch}" - - if remotes.include? user - args.before ['remote', 'set-branches', '--add', user, branch] - args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"] - else - url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'], - :https => https_protocol?) - args.before ['remote', 'add', '-f', '-t', branch, user, url] - end - idx = args.index url_arg - args.delete_at idx - args.insert idx, '--track', '-B', new_branch_name, "#{user}/#{branch}" - end - end - - def merge(args) - _, url_arg = args.words - if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/ - pull_id = $1 - pull_data = api_client.pullrequest_info(url.project, pull_id) - - user, branch = pull_data['head']['label'].split(':', 2) - abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo'] - - url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'], - :https => https_protocol?) - - merge_head = "#{user}/#{branch}" - args.before ['fetch', url, "+refs/heads/#{branch}:refs/remotes/#{merge_head}"] - - idx = args.index url_arg - args.delete_at idx - args.insert idx, merge_head, '--no-ff', '-m', - "Merge pull request ##{pull_id} from #{merge_head}\n\n#{pull_data['title']}" - end - end - - def cherry_pick(args) - unless args.include?('-m') or args.include?('--mainline') - ref = args.words.last - if url = resolve_github_url(ref) and url.project_path =~ /^commit\/([a-f0-9]{7,40})/ - sha = $1 - project = url.project - elsif ref =~ /^(#{OWNER_RE})@([a-f0-9]{7,40})$/ - owner, sha = $1, $2 - project = local_repo.main_project.owned_by(owner) - end - - if project - args[args.index(ref)] = sha - - if remote = project.remote and remotes.include? remote - args.before ['fetch', remote.to_s] - else - args.before ['remote', 'add', '-f', project.owner, project.git_url(:https => https_protocol?)] - end - end - end - end - - def am(args) - if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} } - idx = args.index(url) - gist = $1 == 'gist.' - url = url.sub(/#.+/, '') - url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist - ext = gist ? '.txt' : '.patch' - url += ext unless File.extname(url) == ext - patch_file = File.join(tmp_dir, "#{gist ? 'gist-' : ''}#{File.basename(url)}") - args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file] - args[idx] = patch_file - end - end - - alias_method :apply, :am - - def init(args) - if args.delete('-g') - project = github_project(File.basename(current_dir)) - url = project.git_url(:private => true, :https => https_protocol?) - args.after ['remote', 'add', 'origin', url] - end - end - - def fork(args) - unless project = local_repo.main_project - abort "Error: repository under 'origin' remote is not a GitHub project" - end - forked_project = project.owned_by(github_user(project.host)) - - existing_repo = api_client.repo_info(forked_project) - if existing_repo.success? - parent_data = existing_repo.data['parent'] - parent_url = parent_data && resolve_github_url(parent_data['html_url']) - if !parent_url or parent_url.project != project - abort "Error creating fork: %s already exists on %s" % - [ forked_project.name_with_owner, forked_project.host ] - end - else - api_client.fork_repo(project) unless args.noop? - end - - if args.include?('--no-remote') - exit - else - url = forked_project.git_url(:private => true, :https => https_protocol?) - args.replace %W"remote add -f #{forked_project.owner} #{url}" - args.after 'echo', ['new remote:', forked_project.owner] - end - rescue GitHubAPI::Exceptions - display_api_exception("creating fork", $!.response) - exit 1 - end - - def create(args) - if !is_repo? - abort "'create' must be run from inside a git repository" - else - owner = github_user - args.shift - options = {} - options[:private] = true if args.delete('-p') - new_repo_name = nil - - until args.empty? - case arg = args.shift - when '-d' - options[:description] = args.shift - when '-h' - options[:homepage] = args.shift - else - if arg =~ /^[^-]/ and new_repo_name.nil? - new_repo_name = arg - owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/') - else - abort "invalid argument: #{arg}" - end - end - end - new_repo_name ||= repo_name - new_project = github_project(new_repo_name, owner) - - if api_client.repo_exists?(new_project) - warn "#{new_project.name_with_owner} already exists on #{new_project.host}" - action = "set remote origin" - else - action = "created repository" - unless args.noop? - repo_data = api_client.create_repo(new_project, options) - new_project = github_project(repo_data['full_name']) - end - end - - url = new_project.git_url(:private => true, :https => https_protocol?) - - if remotes.first != 'origin' - args.replace %W"remote add -f origin #{url}" - else - args.replace %W"remote -v" - end - - args.after 'echo', ["#{action}:", new_project.name_with_owner] - end - rescue GitHubAPI::Exceptions - display_api_exception("creating repository", $!.response) - exit 1 - end - - def push(args) - return if args[1].nil? || !args[1].index(',') - - refs = args.words[2..-1] - remotes = args[1].split(',') - args[1] = remotes.shift - - if refs.empty? - refs = [current_branch.short_name] - args.concat refs - end - - remotes.each do |name| - args.after ['push', name, *refs] - end - end - - def browse(args) - args.shift - browse_command(args) do - dest = args.shift - dest = nil if dest == '--' - - if dest - project = github_project dest - branch = master_branch - else - project = current_project - branch = current_branch && current_branch.upstream || master_branch - end - - abort "Usage: hub browse [/]" unless project - - require 'cgi' - path = case subpage = args.shift - when 'commits' - "/commits/#{branch_in_url(branch)}" - when 'tree', NilClass - "/tree/#{branch_in_url(branch)}" if branch and !branch.master? - else - "/#{subpage}" - end - - project.web_url(path) - end - end - - def compare(args) - args.shift - browse_command(args) do - if args.empty? - branch = current_branch.upstream - if branch and not branch.master? - range = branch.short_name - project = current_project - else - abort "Usage: hub compare [USER] [...]" - end - else - sha_or_tag = /((?:#{OWNER_RE}:)?\w[\w.-]+\w)/ - range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2') - project = if owner = args.pop then github_project(nil, owner) - else current_project - end - end - - project.web_url "/compare/#{range}" - end - end - - def hub(args) - return help(args) unless args[1] == 'standalone' - require 'hub/standalone' - Hub::Standalone.build $stdout - exit - rescue LoadError - abort "hub is already running in standalone mode." - rescue Errno::EPIPE - exit # ignore broken pipe - end - - def alias(args) - shells = %w[bash zsh sh ksh csh fish] - - script = !!args.delete('-s') - shell = args[1] || ENV['SHELL'] - abort "hub alias: unknown shell" if shell.nil? or shell.empty? - shell = File.basename shell - - unless shells.include? shell - $stderr.puts "hub alias: unsupported shell" - warn "supported shells: #{shells.join(' ')}" - abort - end - - if script - puts "alias git=hub" - else - profile = case shell - when 'bash' then '~/.bash_profile' - when 'zsh' then '~/.zshrc' - when 'ksh' then '~/.profile' - else - 'your profile' - end - - puts "# Wrap git automatically by adding the following to #{profile}:" - puts - puts 'eval "$(hub alias -s)"' - end - - exit - end - - def version(args) - args.after 'echo', ['hub version', Version] - end - alias_method "--version", :version - - def help(args) - command = args.words[1] - - if command == 'hub' || custom_command?(command) - puts hub_manpage - exit - elsif command.nil? - if args.has_flag?('-a', '--all') - args.after 'echo', ["\nhub custom commands\n"] - args.after 'echo', CUSTOM_COMMANDS.map {|cmd| " #{cmd}" } - else - ENV['GIT_PAGER'] = '' unless args.has_flag?('-p', '--paginate') # Use `cat`. - puts improved_help_text - exit - end - end - end - alias_method "--help", :help - - private - - def branch_in_url(branch) - require 'cgi' - CGI.escape(branch.short_name).gsub("%2F", "/") - end - - def api_client - @api_client ||= begin - config_file = ENV['HUB_CONFIG'] || '~/.config/hub' - file_store = GitHubAPI::FileStore.new File.expand_path(config_file) - file_config = GitHubAPI::Configuration.new file_store - GitHubAPI.new file_config, :app_url => 'http://github.github.com/hub/' - end - end - - def github_user host = nil, &block - host ||= (local_repo(false) || Context::LocalRepo).default_host - api_client.config.username(host, &block) - end - - def custom_command? cmd - CUSTOM_COMMANDS.include? cmd - end - - def respect_help_flags args - return if args.size > 2 - case args[1] - when '-h' - pattern = /(git|hub) #{Regexp.escape args[0].gsub('-', '\-')}/ - hub_raw_manpage.each_line { |line| - if line =~ pattern - $stderr.print "Usage: " - $stderr.puts line.gsub(/\\f./, '').gsub('\-', '-') - abort - end - } - abort "Error: couldn't find usage help for #{args[0]}" - when '--help' - puts hub_manpage - exit - end - end - - def improved_help_text - <<-help -usage: git [--version] [--exec-path[=]] [--html-path] [--man-path] [--info-path] - [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] - [--git-dir=] [--work-tree=] [--namespace=] - [-c name=value] [--help] - [] - -Basic Commands: - init Create an empty git repository or reinitialize an existing one - add Add new or modified files to the staging area - rm Remove files from the working directory and staging area - mv Move or rename a file, a directory, or a symlink - status Show the status of the working directory and staging area - commit Record changes to the repository - -History Commands: - log Show the commit history log - diff Show changes between commits, commit and working tree, etc - show Show information about commits, tags or files - -Branching Commands: - branch List, create, or delete branches - checkout Switch the active branch to another branch - merge Join two or more development histories (branches) together - tag Create, list, delete, sign or verify a tag object - -Remote Commands: - clone Clone a remote repository into a new directory - fetch Download data, tags and branches from a remote repository - pull Fetch from and merge with another repository or a local branch - push Upload data, tags and branches to a remote repository - remote View and manage a set of remote repositories - -Advanced Commands: - reset Reset your staging area or working directory to another point - rebase Re-apply a series of patches in one branch onto another - bisect Find by binary search the change that introduced a bug - grep Print files with lines matching a pattern in your codebase - -GitHub Commands: - pull-request Open a pull request on GitHub - fork Make a fork of a remote repository on GitHub and add as remote - create Create this repository on GitHub and add GitHub as origin - browse Open a GitHub page in the default browser - compare Open a compare page on GitHub - ci-status Show the CI status of a commit - -See 'git help ' for more information on a specific command. -help - end - - def slurp_global_flags(args) - flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ] - flags2 = %w[ --exec-path= --git-dir= --work-tree= ] - - globals = [] - locals = [] - - while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 }) - flag = args.shift - case flag - when '--noop' - args.noop! - when '--version', '--help' - args.unshift flag.sub('--', '') - when '-c' - config_pair = args.shift - key, value = config_pair.split('=', 2) - git_reader.stub_config_value(key, value) - - globals << flag << config_pair - when '-p', '--paginate', '--no-pager' - locals << flag - else - globals << flag - end - end - - git_reader.add_exec_flags(globals) - args.add_exec_flags(globals) - args.add_exec_flags(locals) - end - - def browse_command(args) - url_only = args.delete('-u') - warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p') - url = yield - - args.executable = url_only ? 'echo' : browser_launcher - args.push url - end - - def hub_manpage - abort "** Can't find groff(1)" unless command?('groff') - - require 'open3' - out = nil - Open3.popen3(groff_command) do |stdin, stdout, _| - stdin.puts hub_raw_manpage - stdin.close - out = stdout.read.strip - end - out - end - - def groff_command - "groff -Wall -mtty-char -mandoc -Tascii" - end - - def hub_raw_manpage - if File.exists? file = File.dirname(__FILE__) + '/../../man/hub.1' - File.read(file) - else - DATA.read - end - end - - def puts(*args) - page_stdout - super - end - - def page_stdout - return if not $stdout.tty? or windows? - - read, write = IO.pipe - - if Kernel.fork - $stdin.reopen(read) - read.close - write.close - - ENV['LESS'] = 'FSRX' - - Kernel.select [STDIN] - - pager = ENV['GIT_PAGER'] || - `git config --get-all core.pager`.split("\n").first || - ENV['PAGER'] || - 'less -isr' - - pager = 'cat' if pager.empty? - - exec pager rescue exec "/bin/sh", "-c", pager - else - $stdout.reopen(write) - $stderr.reopen(write) if $stderr.tty? - read.close - write.close - end - rescue NotImplementedError - end - - def pullrequest_editmsg(changes) - message_file = pullrequest_editmsg_file - - if valid_editmsg_file?(message_file) - title, body = read_editmsg(message_file) - previous_message = [title, body].compact.join("\n\n") if title - end - - File.open(message_file, 'w') { |msg| - yield msg, previous_message - if changes - msg.puts "#\n# Changes:\n#" - msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '') - end - } - - edit_cmd = Array(git_editor).dup - edit_cmd << '-c' << 'set ft=gitcommit tw=0 wrap lbr' if edit_cmd[0] =~ /^[mg]?vim$/ - edit_cmd << message_file - system(*edit_cmd) - - unless $?.success? - delete_editmsg(message_file) - abort "error using text editor for pull request message" - end - - title, body = read_editmsg(message_file) - abort "Aborting due to empty pull request title" unless title - [title, body] - end - - def valid_editmsg_file?(message_file) - File.exists?(message_file) && - File.mtime(message_file) > File.mtime(__FILE__) - end - - def read_msg(message) - message.split("\n\n", 2).each {|s| s.strip! }.reject {|s| s.empty? } - end - - def pullrequest_editmsg_file - File.join(git_dir, 'PULLREQ_EDITMSG') - end - - def read_editmsg(file) - title, body = '', '' - File.open(file, 'r') { |msg| - msg.each_line do |line| - next if line.index('#') == 0 - ((body.empty? and line =~ /\S/) ? title : body) << line - end - } - title.tr!("\n", ' ') - title.strip! - body.strip! - - [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil] - end - - def delete_editmsg(file = pullrequest_editmsg_file) - File.delete(file) if File.exist?(file) - end - - def expand_alias(cmd) - if expanded = git_alias_for(cmd) - if expanded.index('!') != 0 - require 'shellwords' unless defined?(::Shellwords) - Shellwords.shellwords(expanded) - end - end - end - - def display_api_exception(action, response) - $stderr.puts "Error #{action}: #{response.message.strip} (HTTP #{response.status})" - if 422 == response.status and response.error_message? - msg = response.error_message - msg = msg.join("\n") if msg.respond_to? :join - warn msg - end - end - - end -end - -module Hub - class Runner - attr_reader :args - - def initialize(*args) - @args = Args.new(args) - Commands.run(@args) - end - - def self.execute(*args) - new(*args).execute - end - - def command - if args.skip? - '' - else - commands.join('; ') - end - end - - def commands - args.commands.map do |cmd| - if cmd.respond_to?(:join) - cmd.map { |arg| arg = arg.to_s; (arg.index(' ') || arg.empty?) ? "'#{arg}'" : arg }.join(' ') - else - cmd.to_s - end - end - end - - def execute - if args.noop? - puts commands - elsif not args.skip? - execute_command_chain args.commands - end - end - - def execute_command_chain commands - commands.each_with_index do |cmd, i| - if cmd.respond_to?(:call) then cmd.call - elsif i == commands.length - 1 - exec(*cmd) - else - exit($?.exitstatus) unless system(*cmd) - end - end - end - - def exec *args - if args.first == 'echo' && Context::windows? - puts args[1..-1].join(' ') - else - super - end - end - end -end - -Hub::Runner.execute(*ARGV) - -__END__ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "HUB" "1" "July 2013" "GITHUB" "Git Manual" -. -.SH "NAME" -\fBhub\fR \- git + hub = github -. -.SH "SYNOPSIS" -\fBhub\fR [\fB\-\-noop\fR] \fICOMMAND\fR \fIOPTIONS\fR -. -.br -\fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR] -. -.SS "Expanded git commands:" -\fBgit init \-g\fR \fIOPTIONS\fR -. -.br -\fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR -. -.br -\fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[/\fIREPOSITORY\fR] -. -.br -\fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR] -. -.br -\fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.] -. -.br -\fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR] -. -.br -\fBgit merge\fR \fIPULLREQ\-URL\fR -. -.br -\fBgit cherry\-pick\fR \fIGITHUB\-REF\fR -. -.br -\fBgit am\fR \fIGITHUB\-URL\fR -. -.br -\fBgit apply\fR \fIGITHUB\-URL\fR -. -.br -\fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR] -. -.br -\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR -. -.SS "Custom git commands:" -\fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR] -. -.br -\fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE] -. -.br -\fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR -. -.br -\fBgit fork\fR [\fB\-\-no\-remote\fR] -. -.br -\fBgit pull\-request\fR [\fB\-f\fR] [\fB\-m\fR \fIMESSAGE\fR|\fB\-F\fR \fIFILE\fR|\fB\-i\fR \fIISSUE\fR|\fIISSUE\-URL\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR] -. -.br -\fBgit ci\-status\fR [\fICOMMIT\fR] -. -.SH "DESCRIPTION" -hub enhances various git commands to ease most common workflows with GitHub\. -. -.TP -\fBhub \-\-noop\fR \fICOMMAND\fR -Shows which command(s) would be run as a result of the current command\. Doesn\'t perform anything\. -. -.TP -\fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR] -Shows shell instructions for wrapping git\. If given, \fISHELL\fR specifies the type of shell; otherwise defaults to the value of SHELL environment variable\. With \fB\-s\fR, outputs shell script suitable for \fBeval\fR\. -. -.TP -\fBgit init\fR \fB\-g\fR \fIOPTIONS\fR -Create a git repository as with git\-init(1) and add remote \fBorigin\fR at "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"; \fIUSER\fR is your GitHub username and \fIREPOSITORY\fR is the current working directory\'s basename\. -. -.TP -\fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR \fIDIRECTORY\fR -Clone repository "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" into \fIDIRECTORY\fR as with git\-clone(1)\. When \fIUSER\fR/ is omitted, assumes your GitHub login\. With \fB\-p\fR, clone private repositories over SSH\. For repositories under your GitHub login, \fB\-p\fR is implicit\. -. -.TP -\fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[\fB/\fR\fIREPOSITORY\fR] -Add remote "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" as with git\-remote(1)\. When /\fIREPOSITORY\fR is omitted, the basename of the current working directory is used\. With \fB\-p\fR, use private remote "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"\. If \fIUSER\fR is "origin" then uses your GitHub login\. -. -.TP -\fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR] -Sets the url of remote \fIREMOTE\-NAME\fR using the same rules as \fBgit remote add\fR\. -. -.TP -\fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.] -Adds missing remote(s) with \fBgit remote add\fR prior to fetching\. New remotes are only added if they correspond to valid forks on GitHub\. -. -.TP -\fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR] -Checks out the head of the pull request as a local branch, to allow for reviewing, rebasing and otherwise cleaning up the commits in the pull request before merging\. The name of the local branch can explicitly be set with \fIBRANCH\fR\. -. -.TP -\fBgit merge\fR \fIPULLREQ\-URL\fR -Merge the pull request with a commit message that includes the pull request ID and title, similar to the GitHub Merge Button\. -. -.TP -\fBgit cherry\-pick\fR \fIGITHUB\-REF\fR -Cherry\-pick a commit from a fork using either full URL to the commit or GitHub\-flavored Markdown notation, which is \fBuser@sha\fR\. If the remote doesn\'t yet exist, it will be added\. A \fBgit fetch \fR is issued prior to the cherry\-pick attempt\. -. -.TP -\fBgit [am|apply]\fR \fIGITHUB\-URL\fR -Downloads the patch file for the pull request or commit at the URL and applies that patch from disk with \fBgit am\fR or \fBgit apply\fR\. Similar to \fBcherry\-pick\fR, but doesn\'t add new remotes\. \fBgit am\fR creates commits while preserving authorship info while \fBapply\fR only applies the patch to the working copy\. -. -.TP -\fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR] -Push \fIREF\fR to each of \fIREMOTE\-1\fR through \fIREMOTE\-N\fR by executing multiple \fBgit push\fR commands\. -. -.TP -\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR -Submodule repository "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" into \fIDIRECTORY\fR as with git\-submodule(1)\. When \fIUSER\fR/ is omitted, assumes your GitHub login\. With \fB\-p\fR, use private remote "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"\. -. -.TP -\fBgit help\fR -Display enhanced git\-help(1)\. -. -.P -hub also adds some custom commands that are otherwise not present in git: -. -.TP -\fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR] -Create a new public GitHub repository from the current git repository and add remote \fBorigin\fR at "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"; \fIUSER\fR is your GitHub username and \fIREPOSITORY\fR is the current working directory name\. To explicitly name the new repository, pass in \fINAME\fR, optionally in \fIORGANIZATION\fR/\fINAME\fR form to create under an organization you\'re a member of\. With \fB\-p\fR, create a private repository, and with \fB\-d\fR and \fB\-h\fR set the repository\'s description and homepage URL, respectively\. -. -.TP -\fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE] -Open repository\'s GitHub page in the system\'s default web browser using \fBopen(1)\fR or the \fBBROWSER\fR env variable\. If the repository isn\'t specified, \fBbrowse\fR opens the page of the repository found in the current directory\. If SUBPAGE is specified, the browser will open on the specified subpage: one of "wiki", "commits", "issues" or other (the default is "tree")\. With \fB\-u\fR, outputs the URL rather than opening the browser\. -. -.TP -\fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR -Open a GitHub compare view page in the system\'s default web browser\. \fISTART\fR to \fIEND\fR are branch names, tag names, or commit SHA1s specifying the range of history to compare\. If a range with two dots (\fBa\.\.b\fR) is given, it will be transformed into one with three dots\. If \fISTART\fR is omitted, GitHub will compare against the base branch (the default is "master")\. With \fB\-u\fR, outputs the URL rather than opening the browser\. -. -.TP -\fBgit fork\fR [\fB\-\-no\-remote\fR] -Forks the original project (referenced by "origin" remote) on GitHub and adds a new remote for it under your username\. -. -.TP -\fBgit pull\-request\fR [\fB\-f\fR] [\fB\-m\fR \fIMESSAGE\fR|\fB\-F\fR \fIFILE\fR|\fB\-i\fR \fIISSUE\fR|\fIISSUE\-URL\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR] -Opens a pull request on GitHub for the project that the "origin" remote points to\. The default head of the pull request is the current branch\. Both base and head of the pull request can be explicitly given in one of the following formats: "branch", "owner:branch", "owner/repo:branch"\. This command will abort operation if it detects that the current topic branch has local commits that are not yet pushed to its upstream branch on the remote\. To skip this check, use \fB\-f\fR\. -. -.IP -Without \fIMESSAGE\fR or \fIFILE\fR, a text editor will open in which title and body of the pull request can be entered in the same manner as git commit message\. Pull request message can also be passed via stdin with \fB\-F \-\fR\. -. -.IP -If instead of normal \fITITLE\fR an issue number is given with \fB\-i\fR, the pull request will be attached to an existing GitHub issue\. Alternatively, instead of title you can paste a full URL to an issue on GitHub\. -. -.TP -\fBgit ci\-status\fR [\fICOMMIT\fR] -Looks up the SHA for \fICOMMIT\fR in GitHub Status API and displays the latest status\. Exits with one of: -. -.br -success (0), error (1), failure (1), pending (2), no status (3) -. -.SH "CONFIGURATION" -Hub will prompt for GitHub username & password the first time it needs to access the API and exchange it for an OAuth token, which it saves in "~/\.config/hub"\. -. -.P -To avoid being prompted, use \fIGITHUB_USER\fR and \fIGITHUB_PASSWORD\fR environment variables\. -. -.P -If you prefer the HTTPS protocol for GitHub repositories, you can set "hub\.protocol" to "https"\. This will affect \fBclone\fR, \fBfork\fR, \fBremote add\fR and other operations that expand references to GitHub repositories as full URLs that otherwise use git and ssh protocols\. -. -.IP "" 4 -. -.nf - -$ git config \-\-global hub\.protocol https -. -.fi -. -.IP "" 0 -. -.SS "GitHub Enterprise" -By default, hub will only work with repositories that have remotes which point to github\.com\. GitHub Enterprise hosts need to be whitelisted to configure hub to treat such remotes same as github\.com: -. -.IP "" 4 -. -.nf - -$ git config \-\-global \-\-add hub\.host my\.git\.org -. -.fi -. -.IP "" 0 -. -.P -The default host for commands like \fBinit\fR and \fBclone\fR is still github\.com, but this can be affected with the \fIGITHUB_HOST\fR environment variable: -. -.IP "" 4 -. -.nf - -$ GITHUB_HOST=my\.git\.org git clone myproject -. -.fi -. -.IP "" 0 -. -.SH "EXAMPLES" -. -.SS "git clone" -. -.nf - -$ git clone schacon/ticgit -> git clone git://github\.com/schacon/ticgit\.git - -$ git clone \-p schacon/ticgit -> git clone git@github\.com:schacon/ticgit\.git - -$ git clone resque -> git clone git@github\.com/YOUR_USER/resque\.git -. -.fi -. -.SS "git remote add" -. -.nf - -$ git remote add rtomayko -> git remote add rtomayko git://github\.com/rtomayko/CURRENT_REPO\.git - -$ git remote add \-p rtomayko -> git remote add rtomayko git@github\.com:rtomayko/CURRENT_REPO\.git - -$ git remote add origin -> git remote add origin git://github\.com/YOUR_USER/CURRENT_REPO\.git -. -.fi -. -.SS "git fetch" -. -.nf - -$ git fetch mislav -> git remote add mislav git://github\.com/mislav/REPO\.git -> git fetch mislav - -$ git fetch mislav,xoebus -> git remote add mislav \.\.\. -> git remote add xoebus \.\.\. -> git fetch \-\-multiple mislav xoebus -. -.fi -. -.SS "git cherry\-pick" -. -.nf - -$ git cherry\-pick http://github\.com/mislav/REPO/commit/SHA -> git remote add \-f mislav git://github\.com/mislav/REPO\.git -> git cherry\-pick SHA - -$ git cherry\-pick mislav@SHA -> git remote add \-f mislav git://github\.com/mislav/CURRENT_REPO\.git -> git cherry\-pick SHA - -$ git cherry\-pick mislav@SHA -> git fetch mislav -> git cherry\-pick SHA -. -.fi -. -.SS "git am, git apply" -. -.nf - -$ git am https://github\.com/defunkt/hub/pull/55 -> curl https://github\.com/defunkt/hub/pull/55\.patch \-o /tmp/55\.patch -> git am /tmp/55\.patch - -$ git am \-\-ignore\-whitespace https://github\.com/davidbalbert/hub/commit/fdb9921 -> curl https://github\.com/davidbalbert/hub/commit/fdb9921\.patch \-o /tmp/fdb9921\.patch -> git am \-\-ignore\-whitespace /tmp/fdb9921\.patch - -$ git apply https://gist\.github\.com/8da7fb575debd88c54cf -> curl https://gist\.github\.com/8da7fb575debd88c54cf\.txt \-o /tmp/gist\-8da7fb575debd88c54cf\.txt -> git apply /tmp/gist\-8da7fb575debd88c54cf\.txt -. -.fi -. -.SS "git fork" -. -.nf - -$ git fork -[ repo forked on GitHub ] -> git remote add \-f YOUR_USER git@github\.com:YOUR_USER/CURRENT_REPO\.git -. -.fi -. -.SS "git pull\-request" -. -.nf - -# while on a topic branch called "feature": -$ git pull\-request -[ opens text editor to edit title & body for the request ] -[ opened pull request on GitHub for "YOUR_USER:feature" ] - -# explicit title, pull base & head: -$ git pull\-request \-m "Implemented feature X" \-b defunkt:master \-h mislav:feature - -$ git pull\-request \-i 123 -[ attached pull request to issue #123 ] -. -.fi -. -.SS "git checkout" -. -.nf - -$ git checkout https://github\.com/defunkt/hub/pull/73 -> git remote add \-f \-t feature git://github:com/mislav/hub\.git -> git checkout \-\-track \-B mislav\-feature mislav/feature - -$ git checkout https://github\.com/defunkt/hub/pull/73 custom\-branch\-name -. -.fi -. -.SS "git merge" -. -.nf - -$ git merge https://github\.com/defunkt/hub/pull/73 -> git fetch git://github\.com/mislav/hub\.git +refs/heads/feature:refs/remotes/mislav/feature -> git merge mislav/feature \-\-no\-ff \-m \'Merge pull request #73 from mislav/feature\.\.\.\' -. -.fi -. -.SS "git create" -. -.nf - -$ git create -[ repo created on GitHub ] -> git remote add origin git@github\.com:YOUR_USER/CURRENT_REPO\.git - -# with description: -$ git create \-d \'It shall be mine, all mine!\' - -$ git create recipes -[ repo created on GitHub ] -> git remote add origin git@github\.com:YOUR_USER/recipes\.git - -$ git create sinatra/recipes -[ repo created in GitHub organization ] -> git remote add origin git@github\.com:sinatra/recipes\.git -. -.fi -. -.SS "git init" -. -.nf - -$ git init \-g -> git init -> git remote add origin git@github\.com:YOUR_USER/REPO\.git -. -.fi -. -.SS "git push" -. -.nf - -$ git push origin,staging,qa bert_timeout -> git push origin bert_timeout -> git push staging bert_timeout -> git push qa bert_timeout -. -.fi -. -.SS "git browse" -. -.nf - -$ git browse -> open https://github\.com/YOUR_USER/CURRENT_REPO - -$ git browse \-\- commit/SHA -> open https://github\.com/YOUR_USER/CURRENT_REPO/commit/SHA - -$ git browse \-\- issues -> open https://github\.com/YOUR_USER/CURRENT_REPO/issues - -$ git browse schacon/ticgit -> open https://github\.com/schacon/ticgit - -$ git browse schacon/ticgit commit/SHA -> open https://github\.com/schacon/ticgit/commit/SHA - -$ git browse resque -> open https://github\.com/YOUR_USER/resque - -$ git browse resque network -> open https://github\.com/YOUR_USER/resque/network -. -.fi -. -.SS "git compare" -. -.nf - -$ git compare refactor -> open https://github\.com/CURRENT_REPO/compare/refactor - -$ git compare 1\.0\.\.1\.1 -> open https://github\.com/CURRENT_REPO/compare/1\.0\.\.\.1\.1 - -$ git compare \-u fix -> (https://github\.com/CURRENT_REPO/compare/fix) - -$ git compare other\-user patch -> open https://github\.com/other\-user/REPO/compare/patch -. -.fi -. -.SS "git submodule" -. -.nf - -$ hub submodule add wycats/bundler vendor/bundler -> git submodule add git://github\.com/wycats/bundler\.git vendor/bundler - -$ hub submodule add \-p wycats/bundler vendor/bundler -> git submodule add git@github\.com:wycats/bundler\.git vendor/bundler - -$ hub submodule add \-b ryppl \-\-name pip ryppl/pip vendor/pip -> git submodule add \-b ryppl \-\-name pip git://github\.com/ryppl/pip\.git vendor/pip -. -.fi -. -.SS "git ci\-status" -. -.nf - -$ hub ci\-status [commit] -> (prints CI state of commit and exits with appropriate code) -> One of: success (0), error (1), failure (1), pending (2), no status (3) -. -.fi -. -.SS "git help" -. -.nf - -$ git help -> (improved git help) -$ git help hub -> (hub man page) -. -.fi -. -.SH "BUGS" -\fIhttps://github\.com/github/hub/issues\fR -. -.SH "AUTHORS" -\fIhttps://github\.com/github/hub/contributors\fR -. -.SH "SEE ALSO" -git(1), git\-clone(1), git\-remote(1), git\-init(1), \fIhttp://github\.com\fR, \fIhttps://github\.com/github/hub\fR