module Twig::Cli

Handles raw input from the command-line interface.

Public Class Methods

prompt_with_choices(prompt, choices) click to toggle source
# File lib/twig/cli.rb, line 8
def self.prompt_with_choices(prompt, choices)
  # Prints the given string `prompt` and the array `choices` numbered, and
  # prompts the user to enter a number. Returns the matching value, or nil
  # if the user input is invalid.

  raise ArgumentError, 'At least two choices required' if choices.size < 2

  puts prompt
  choices.each_with_index do |choice, index|
    puts "#{sprintf('%3s', index + 1)}. #{choice}"
  end
  print '> '

  input = $stdin.gets.to_i
  choices[input - 1]
end

Public Instance Methods

abort_for_option_exception(exception) click to toggle source
# File lib/twig/cli.rb, line 341
def abort_for_option_exception(exception)
  puts exception.message + "\nFor a list of options, run `twig help`."
  exit 1
end
get_branch_property_for_cli(branch_name, property_name) click to toggle source
# File lib/twig/cli.rb, line 380
def get_branch_property_for_cli(branch_name, property_name)
  value = get_branch_property(branch_name, property_name)
  if value
    puts value
  else
    raise Twig::Branch::MissingPropertyError,
      %{The branch "#{branch_name}" does not have the property "#{property_name}".}
  end
rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
  abort exception.message
end
read_cli_args!(args) click to toggle source
# File lib/twig/cli.rb, line 346
def read_cli_args!(args)
  Twig::Subcommands.exec_subcommand_if_any(args) if args.any?

  args = read_cli_options!(args)
  branch_name = target_branch_name
  format = options.delete(:format)
  property_to_unset = options.delete(:unset_property)

  # Handle remaining arguments, if any
  if args.any?
    property_name, property_value = args[0], args[1]

    read_cli_options!(args)

    # Get/set branch property
    if property_value
      # `$ twig <key> <value>`
      set_branch_property_for_cli(branch_name, property_name, property_value)
    else
      # `$ twig <key>`
      get_branch_property_for_cli(branch_name, property_name)
    end
  elsif property_to_unset
    # `$ twig --unset <key>`
    unset_branch_property_for_cli(branch_name, property_to_unset)
  elsif format == :json
    # `$ twig --format json`
    puts branches_json
  else
    # `$ twig`
    puts list_branches
  end
end
read_cli_options!(args) click to toggle source
# File lib/twig/cli.rb, line 55
    def read_cli_options!(args)
      custom_properties = Twig::Branch.all_property_names

      option_parser = OptionParser.new do |opts|
        opts.banner         = Help.intro
        opts.summary_indent = ' ' * 2
        opts.summary_width  = 32

        ###

        Help.header(opts, 'Common options')

        desc = 'Use a specific branch.'
        opts.on(
          '-b BRANCH', '--branch BRANCH', *Help.description(desc)
        ) do |branch|
          set_option(:branch, branch)
        end

        desc = 'Unset a branch property.'
        opts.on('--unset PROPERTY', *Help.description(desc)) do |property_name|
          set_option(:unset_property, property_name)
        end

        desc = 'Show this help content.'
        opts.on('--help', *Help.description(desc)) do
          summary_lines = opts.to_s.split("\n")
          run_pager

          # Filter out custom property lines
          prev_line = nil
          summary_lines.each do |line|
            # Squash successive blank lines
            next if line == "\n" && prev_line == "\n"

            unless Help.line_for_custom_property?(line)
              puts line
              prev_line = line
            end
          end

          exit
        end

        desc = 'Show Twig version.'
        opts.on('--version', *Help.description(desc)) do
          puts Twig::VERSION
          exit
        end

        ###

        Help.header(opts, 'Filtering branches')

        desc = 'Only list branches below a given age.'
        opts.on(
          '--max-days-old AGE', *Help.description(desc, :add_blank_line => true)
        ) do |age|
          set_option(:max_days_old, age)
        end

        desc = 'Only list branches whose name matches a given pattern.'
        opts.on(
          '--only-branch PATTERN',
          *Help.description(desc, :add_blank_line => true)
        ) do |pattern|
          set_option(:property_only, :branch => pattern)
        end

        desc = 'Do not list branches whose name matches a given pattern.'
        opts.on('--except-branch PATTERN', *Help.description(desc)) do |pattern|
          set_option(:property_except, :branch => pattern)
        end

        custom_properties.each do |property_name|
          property_name_sym = property_name.to_sym

          opts.on("--only-#{property_name} PATTERN") do |pattern|
            set_option(:property_only, property_name_sym => pattern)
          end

          opts.on("--except-#{property_name} PATTERN") do |pattern|
            set_option(:property_except, property_name_sym => pattern)
          end
        end
        Help.description_for_custom_property(opts, [
          ['--only-PROPERTY PATTERN',   'Only list branches with a given property'],
          ['',                          'that matches a given pattern.']
        ], :trailing => '')
        Help.description_for_custom_property(opts, [
          ['--except-PROPERTY PATTERN', 'Do not list branches with a given property'],
          ['',                          'that matches a given pattern.']
        ])

        desc =
          'Print branch properties in a format that can be used by other ' \
          'tools. Currently, the only supported value is `json`.'
        opts.on(
          '--format FORMAT', *Help.description(desc, :add_blank_line => true)
        ) do |format|
          set_option(:format, format)
        end

        desc =
          'Lists all branches regardless of other filtering options. ' \
          'Useful for overriding options in ' +
          File.basename(Twig::Options::CONFIG_PATH) + '.'
        opts.on('--all', *Help.description(desc)) do |pattern|
          unset_option(:max_days_old)
          unset_option(:property_except)
          unset_option(:property_only)
        end

        ###

        Help.header(opts, 'Listing branches')

        desc = <<-DESC
          Set the width for the `branch` column.
          (Default: #{Twig::DEFAULT_BRANCH_COLUMN_WIDTH})
        DESC
        opts.on('--branch-width NUMBER', *Help.description(desc)) do |width|
          set_option(:property_width, :branch => width)
        end

        custom_properties.each do |property_name|
          opts.on("--#{property_name}-width NUMBER") do |width|
            set_option(:property_width, property_name.to_sym => width)
          end
        end
        Help.description_for_custom_property(opts, [
          ['--PROPERTY-width NUMBER', "Set the width for a given property's column."],
          ['',                        "(Default: #{Twig::DEFAULT_PROPERTY_COLUMN_WIDTH})"]
        ])

        desc = <<-DESC
          Only include properties where the property name matches the given
          regular expression.
        DESC
        opts.on(
          '--only-property PATTERN',
          *Help.description(desc, :add_blank_line => true)
        ) do |pattern|
          set_option(:property_only_name, pattern)
        end

        desc = <<-DESC
          Exclude properties where the property name matches the given regular
          expression.
        DESC
        opts.on(
          '--except-property PATTERN',
          *Help.description(desc, :add_blank_line => true)
        ) do |pattern|
          set_option(:property_except_name, pattern)
        end

        colors = Twig::Display::COLORS.keys.
          map { |value| format_string(value, :color => value) }.
          join(', ')
        weights = Twig::Display::WEIGHTS.keys.
          map { |value| format_string(value, :weight => value) }.
          join(' and ')
        default_header_style = format_string(
          Twig::DEFAULT_HEADER_COLOR.to_s,
          :color => Twig::DEFAULT_HEADER_COLOR
        )
        desc = <<-DESC
          STYLE is a color, weight, or a space-separated pair of one of each.
          Valid colors are #{colors}. Valid weights are #{weights}.
          (Default: "#{default_header_style}")
        DESC
        opts.on(
          '--header-style "STYLE"',
          *Help.description(desc, :add_blank_line => true)
        ) do |style|
          set_option(:header_style, style)
        end

        desc = 'Show oldest branches first. (Default: false)'
        opts.on('--reverse', *Help.description(desc)) do
          set_option(:reverse, true)
        end

        ###

        Help.header(opts, 'GitHub integration')

        desc = <<-DESC
          Set a custom GitHub API URI prefix, e.g.,
          https://github-enterprise.example.com/api/v3.
          (Default: "#{Twig::DEFAULT_GITHUB_API_URI_PREFIX}")
        DESC
        opts.on(
          '--github-api-uri-prefix PREFIX',
          *Help.description(desc, :add_blank_line => true)
        ) do |prefix|
          set_option(:github_api_uri_prefix, prefix)
        end

        desc = <<-DESC
          Set a custom GitHub URI prefix, e.g.,
          https://github-enterprise.example.com.
          (Default: "#{Twig::DEFAULT_GITHUB_URI_PREFIX}")
        DESC
        opts.on('--github-uri-prefix PREFIX', *Help.description(desc)) do |prefix|
          set_option(:github_uri_prefix, prefix)
        end

        ###

        Help.header(opts, 'Config files and tab completion', :trailing => '')

        Help.print_paragraph(opts, %{
          Twig can automatically set up a config file for you, where you can put
          your most frequently used options for filtering and listing branches.
          To get started, run `twig init` and follow the instructions. This does
          two things:
        })

        Help.print_paragraph(opts, %{
          * Creates #{Twig::Options::CONFIG_PATH}, where you can put your
            favorite options, e.g.:
        })

        Help.print_section(opts, [
          '      except-branch: staging',
          '      header-style:  green bold',
          '      max-days-old:  30',
          '      reverse:       true'
        ].join("\n"))

        Help.print_paragraph(opts, %{
          * Enables tab completion for Twig subcommands and branch names, e.g.:
        })

        Help.print_section(opts, [
          '      `twig cre<tab>` -> `twig create-branch`',
          '      `twig status -b my-br<tab>` -> `twig status -b my-branch`'
        ].join("\n"), :trailing => '')

        ###

        Help.header(opts, 'Subcommands', :trailing => '')

        Help.print_paragraph(opts, "Twig comes with these subcommands:", :trailing => "\n\n")

        Help.subcommand_descriptions.each do |desc|
          Help.print_line(opts, desc)
        end

        Help.subheader(opts, 'Writing a subcommand', :trailing => '')

        Help.print_paragraph(opts, %{
          You can write any Twig subcommand that fits your own Git workflow. To
          write a Twig subcommand:
        })

        Help.print_section(opts, [
          '1.  Write a script; any language will do. (If you want to take',
          '    advantage of Twig\'s option parsing and branch processing, you\'ll',
          '    need Ruby. See `twig-checkout-parent` for an example.)'
        ].join("\n"))

        Help.print_section(opts, [
          '2.  Save it with the `twig-` prefix in your `$PATH`,',
          '    e.g., `~/bin/twig-my-subcommand`.'
        ].join("\n"))

        Help.print_section(opts, [
          '3.  Make it executable: `chmod ugo+x ~/bin/twig-my-subcommand`'
        ].join("\n"))

        Help.print_section(opts, [
          '4.  Run your subcommand: `twig my-subcommand` (with a *space* after',
          '    `twig`'
        ].join("\n"))
      end

      option_parser.parse!(args)
    rescue OptionParser::InvalidOption, OptionParser::MissingArgument => exception
      abort_for_option_exception(exception)
    ensure
      args
    end
run_pager() click to toggle source
# File lib/twig/cli.rb, line 25
def run_pager
  # Starts a pager so that all following STDOUT output is paginated.
  # Based on: http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby

  return if Twig::System.windows? || !$stdout.tty? || !Kernel.respond_to?(:fork)

  read_io, write_io = IO.pipe

  # Create child process
  unless Kernel.fork
    # The following runs only in the child process:
    $stdout.reopen(write_io)
    $stderr.reopen(write_io) if $stderr.tty?
    read_io.close
    write_io.close
    return
  end

  $stdin.reopen(read_io)
  read_io.close
  write_io.close

  ENV['LESS'] = 'FSRX'      # Don't page if the input fits on screen
  Kernel.select([$stdin])   # Wait for input before starting pager

  # Turn parent process into pager
  pager = ENV['PAGER'] || 'less'
  exec pager rescue exec '/bin/sh', '-c', pager
end
set_branch_property_for_cli(branch_name, property_name, property_value) click to toggle source
# File lib/twig/cli.rb, line 392
def set_branch_property_for_cli(branch_name, property_name, property_value)
  puts set_branch_property(branch_name, property_name, property_value)
rescue ArgumentError, RuntimeError => exception
  abort exception.message
end
unset_branch_property_for_cli(branch_name, property_name) click to toggle source
# File lib/twig/cli.rb, line 398
def unset_branch_property_for_cli(branch_name, property_name)
  puts unset_branch_property(branch_name, property_name)
rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
  abort exception.message
end