├── .gitignore ├── .hound.yml ├── .rubocop.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── INSTALL ├── INSTALL.in ├── LICENSE ├── Makefile.am ├── README.md ├── arch ├── PKGBUILD └── PKGBUILD.in ├── assets ├── gitsh-logo.png └── gitsh-logo.svg ├── autogen.sh ├── bin ├── gitsh └── tcviz ├── configure.ac ├── configure.rb ├── etc └── completions ├── ext └── gitsh │ ├── extconf.rb │ └── src │ └── line_editor.c ├── homebrew └── gitsh.rb.in ├── lib └── gitsh │ ├── argument_list.rb │ ├── arguments │ ├── composite_argument.rb │ ├── string_argument.rb │ ├── subshell.rb │ └── variable_argument.rb │ ├── capturing_environment.rb │ ├── cli.rb │ ├── colors.rb │ ├── commands │ ├── git_command.rb │ ├── internal_command.rb │ ├── lazy_command.rb │ ├── noop.rb │ ├── shell_command.rb │ └── tree.rb │ ├── environment.rb │ ├── error.rb │ ├── exit_statuses.rb │ ├── file_runner.rb │ ├── git_command_list.rb │ ├── git_repository.rb │ ├── git_repository │ └── status.rb │ ├── history.rb │ ├── input_strategies │ ├── file.rb │ └── interactive.rb │ ├── interpreter.rb │ ├── lexer.rb │ ├── lexer │ └── character_class.rb │ ├── line_editor.rb │ ├── line_editor_history_filter.rb │ ├── magic_variables.rb │ ├── module_delegator.rb │ ├── parser.rb │ ├── prompt_color.rb │ ├── prompter.rb │ ├── quote_detector.rb │ ├── shell_command_runner.rb │ ├── tab_completion │ ├── README.md │ ├── alias_expander.rb │ ├── automaton.rb │ ├── automaton_factory.rb │ ├── command_completer.rb │ ├── context.rb │ ├── dsl.rb │ ├── dsl │ │ ├── choice_factory.rb │ │ ├── concatenation_factory.rb │ │ ├── fallback_transition_factory.rb │ │ ├── lexer.rb │ │ ├── maybe_operation_factory.rb │ │ ├── null_factory.rb │ │ ├── option_transition_factory.rb │ │ ├── parse_error.rb │ │ ├── parser.rb │ │ ├── plus_operation_factory.rb │ │ ├── rule_factory.rb │ │ ├── rule_set_factory.rb │ │ ├── star_operation_factory.rb │ │ ├── text_transition_factory.rb │ │ └── variable_transition_factory.rb │ ├── escaper.rb │ ├── facade.rb │ ├── matchers │ │ ├── anything_matcher.rb │ │ ├── base_matcher.rb │ │ ├── branch_matcher.rb │ │ ├── command_matcher.rb │ │ ├── path_matcher.rb │ │ ├── remote_matcher.rb │ │ ├── revision_matcher.rb │ │ ├── tag_matcher.rb │ │ ├── text_matcher.rb │ │ └── unknown_option_matcher.rb │ ├── tokens_to_words.rb │ ├── variable_completer.rb │ └── visualization.rb │ ├── terminal.rb │ └── version.rb.in ├── m4 ├── ax_compare_version.m4 └── ax_prog_ruby_version.m4 ├── man ├── man1 │ └── gitsh.1.in └── man5 │ └── gitsh_completions.5.in ├── spec ├── fixtures │ └── fake_git ├── integration │ ├── arguments_spec.rb │ ├── cd_command_spec.rb │ ├── chaining_spec.rb │ ├── coloring_spec.rb │ ├── command_arguments_spec.rb │ ├── comment_spec.rb │ ├── completion_errors_spec.rb │ ├── correction_spec.rb │ ├── create_repository_spec.rb │ ├── default_command_spec.rb │ ├── default_git_path_spec.rb │ ├── error_handling_spec.rb │ ├── escaping_spec.rb │ ├── gitshrc_spec.rb │ ├── greeting_spec.rb │ ├── help_command_spec.rb │ ├── inputrc_spec.rb │ ├── magic_variables_spec.rb │ ├── multi_line_input_spec.rb │ ├── persistent_history_spec.rb │ ├── prompt_spec.rb │ ├── running_scripts_spec.rb │ ├── shell_commands_spec.rb │ ├── source_command_spec.rb │ ├── subshell_spec.rb │ ├── tab_completion_spec.rb │ └── variables_spec.rb ├── spec_helper.rb ├── support │ ├── colors.rb │ ├── delegate_matcher.rb │ ├── execute_matcher.rb │ ├── fake_line_editor.rb │ ├── file_system.rb │ ├── fixtures.rb │ ├── gitsh_runner.rb │ ├── internal_command_shared_examples.rb │ ├── produce_tokens_matcher.rb │ ├── scripts.rb │ ├── signalling_line_editor.rb │ ├── stubbed_method_result.rb │ ├── tab_completion.rb │ ├── tokens.rb │ └── working_directory.rb └── units │ ├── argument_list_spec.rb │ ├── arguments │ ├── composite_argument_spec.rb │ ├── string_argument_spec.rb │ ├── subshell_spec.rb │ └── variable_argument_spec.rb │ ├── capturing_environment_spec.rb │ ├── cli_spec.rb │ ├── commands │ ├── git_command_spec.rb │ ├── internal │ │ ├── chdir_spec.rb │ │ ├── echo_spec.rb │ │ ├── exit_spec.rb │ │ ├── help_spec.rb │ │ ├── set_spec.rb │ │ ├── source_spec.rb │ │ └── unknown_spec.rb │ ├── internal_command_spec.rb │ ├── lazy_command_spec.rb │ ├── shell_command_spec.rb │ └── tree_spec.rb │ ├── environment_spec.rb │ ├── file_runner_spec.rb │ ├── git_command_list_spec.rb │ ├── git_repository │ └── status_spec.rb │ ├── git_repository_spec.rb │ ├── history_spec.rb │ ├── input_strategies │ ├── file_spec.rb │ └── interactive_spec.rb │ ├── interpreter_spec.rb │ ├── lexer │ └── character_class_spec.rb │ ├── lexer_spec.rb │ ├── line_editor_history_filter_spec.rb │ ├── line_editor_spec.rb │ ├── magic_variables_spec.rb │ ├── parser_spec.rb │ ├── prompt_color_spec.rb │ ├── prompter_spec.rb │ ├── quote_detector_spec.rb │ ├── shell_command_runner_spec.rb │ ├── tab_completion │ ├── alias_expander_spec.rb │ ├── automaton_factory_spec.rb │ ├── automaton_spec.rb │ ├── command_completer_spec.rb │ ├── context_spec.rb │ ├── dsl │ │ ├── choice_factory_spec.rb │ │ ├── concatenation_factory_spec.rb │ │ ├── fallback_transition_factory_spec.rb │ │ ├── lexer_spec.rb │ │ ├── maybe_operation_factory_spec.rb │ │ ├── option_transition_factory_spec.rb │ │ ├── parse_error_spec.rb │ │ ├── parser_spec.rb │ │ ├── plus_operation_factory_spec.rb │ │ ├── rule_factory_spec.rb │ │ ├── rule_set_factory_spec.rb │ │ ├── star_operation_factory_spec.rb │ │ ├── text_transition_factory_spec.rb │ │ └── variable_transition_factory_spec.rb │ ├── dsl_spec.rb │ ├── escaper_spec.rb │ ├── facade_spec.rb │ ├── matchers │ │ ├── anything_matcher_spec.rb │ │ ├── branch_matchers_spec.rb │ │ ├── command_matcher_spec.rb │ │ ├── path_matcher_spec.rb │ │ ├── remote_matcher_spec.rb │ │ ├── revision_matcher_spec.rb │ │ ├── tag_matchers_spec.rb │ │ ├── text_matcher_spec.rb │ │ └── unknown_option_matcher_spec.rb │ ├── tokens_to_words_spec.rb │ ├── variable_completer_spec.rb │ └── visualization_spec.rb │ └── terminal_spec.rb ├── src ├── gitsh.c └── gitsh.rb.in └── vendor └── vendorize /.gitignore: -------------------------------------------------------------------------------- 1 | # autotools related files 2 | Makefile 3 | Makefile.in 4 | */Makefile 5 | */Makefile.in 6 | aclocal.m4 7 | autom4te.cache 8 | autoscan.log 9 | config.log 10 | config.status 11 | configure 12 | configure.scan 13 | install-sh 14 | missing 15 | compile 16 | depcomp 17 | test-driver 18 | */.deps 19 | */.dirstamp 20 | 21 | # Make targets 22 | /gitsh 23 | src/gitsh.rb 24 | lib/gitsh/version.rb 25 | man/man1/gitsh.1 26 | man/man5/gitsh_completions.5 27 | gitsh-*.tar.gz 28 | vendor/gems 29 | *.o 30 | 31 | # Ruby C-extension 32 | *.bundle 33 | *.so 34 | mkmf.log 35 | .RUBYARCHDIR* 36 | 37 | # Sub-repositories for release 38 | gh-pages 39 | homebrew-formulae 40 | release-arch 41 | 42 | # Test output 43 | spec/**/*log 44 | test-suite.log 45 | spec/**/*trs 46 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ruby: 2 | config_file: .rubocop.yml 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Layout/IndentArray: 2 | Description: Checks the indentation of the first element in an array literal. 3 | Enabled: true 4 | EnforcedStyle: consistent 5 | 6 | Style/StringLiterals: 7 | Description: Checks if uses of quotes match the configured preference. 8 | StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals 9 | Enabled: true 10 | EnforcedStyle: single_quotes 11 | SupportedStyles: 12 | - single_quotes 13 | - double_quotes 14 | 15 | Style/CollectionMethods: 16 | Enabled: true 17 | PreferredMethods: 18 | find: detect 19 | reduce: inject 20 | collect: map 21 | find_all: select 22 | 23 | Naming/VariableNumber: 24 | Description: Use normalcase for variable numbers. 25 | Enabled: false 26 | 27 | Naming/MemoizedInstanceVariableName: 28 | EnforcedStyleForLeadingUnderscores: required 29 | 30 | Style/TrailingCommaInArguments: 31 | Description: 'Checks for trailing comma in argument lists.' 32 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' 33 | EnforcedStyleForMultiline: consistent_comma 34 | SupportedStylesForMultiline: 35 | - comma 36 | - consistent_comma 37 | - no_comma 38 | Enabled: true 39 | 40 | Style/TrailingCommaInArrayLiteral: 41 | EnforcedStyleForMultiline: consistent_comma 42 | Enabled: true 43 | 44 | Style/TrailingCommaInHashLiteral: 45 | EnforcedStyleForMultiline: consistent_comma 46 | Enabled: true 47 | 48 | Style/EmptyMethod: 49 | Enabled: false 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.3" 4 | - "2.4" 5 | - "2.5" 6 | - "2.6" 7 | install: 8 | - bundle install 9 | - ./autogen.sh 10 | - git config --global user.name 'Travis' 11 | - git config --global user.email 'travis@example.com' 12 | - export LD_LIBRARY_PATH="${MY_RUBY_HOME}/lib" 13 | script: 14 | - RUBY=$(which ruby) ./configure --prefix=$PWD/tmp 15 | - make 16 | - make install 17 | - echo :help | ./tmp/bin/gitsh 18 | - bundle exec rspec 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | By participating in this project, you agree to abide by the [thoughtbot code of conduct][1]. 4 | 5 | [1]: https://thoughtbot.com/open-source-code-of-conduct 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :dist do 4 | gem 'rltk' 5 | end 6 | 7 | group :test do 8 | gem 'rspec' 9 | gem 'pry' 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coderay (1.1.2) 5 | diff-lcs (1.3) 6 | ffi (1.10.0) 7 | filigree (0.3.3) 8 | method_source (0.9.2) 9 | pry (0.12.2) 10 | coderay (~> 1.1.0) 11 | method_source (~> 0.9.0) 12 | rltk (3.0.1) 13 | ffi (>= 1.0.0) 14 | filigree (>= 0.2.0) 15 | rspec (3.8.0) 16 | rspec-core (~> 3.8.0) 17 | rspec-expectations (~> 3.8.0) 18 | rspec-mocks (~> 3.8.0) 19 | rspec-core (3.8.0) 20 | rspec-support (~> 3.8.0) 21 | rspec-expectations (3.8.2) 22 | diff-lcs (>= 1.2.0, < 2.0) 23 | rspec-support (~> 3.8.0) 24 | rspec-mocks (3.8.0) 25 | diff-lcs (>= 1.2.0, < 2.0) 26 | rspec-support (~> 3.8.0) 27 | rspec-support (3.8.0) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | pry 34 | rltk 35 | rspec 36 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | gitsh installation 2 | ================== 3 | 4 | The ideal way to install gitsh is via your operating system's package manager. 5 | Currently gitsh packages are available for: 6 | 7 | * macOS (via Homebrew) 8 | * Arch Linux 9 | * OpenBSD (-current) 10 | 11 | On other operating systems you should install using the tarball, following the 12 | instructions in this guide. 13 | 14 | 15 | Prerequisites 16 | ------------- 17 | 18 | * Ruby version 2.3.0 or later 19 | * gcc or a similar C compiler 20 | * GNU Readline 21 | 22 | 23 | gitsh and Ruby version managers 24 | ------------------------------- 25 | 26 | The gitsh configuration script will attempt to find a system wide version of 27 | Ruby 2.3.0 or later. Rubies installed by Ruby version managers will usually be 28 | ignored to avoid problems when those binaries are moved or deleted. 29 | 30 | To force gitsh to use a specific Ruby binary, set the $RUBY environment variable 31 | when running the configuration script. For example, this will use the first ruby 32 | binary on the $PATH: 33 | 34 | RUBY=$(which ruby) ./configure 35 | 36 | 37 | GNU Readline 38 | ------------ 39 | 40 | gitsh provides a Ruby C extension that integrates with GNU Readline. Ruby ships 41 | with its own Readline module, but it doesn't offer all of the features required 42 | for comprehensive tab-completion. 43 | 44 | Other line editor libraries, like libedit, are partly compatible with GNU 45 | Readline, but don't offer enough features to be supported by gitsh. 46 | 47 | The configure script may fail if GNU Readline isn't found. You can set the 48 | $CPPFLAGS and $LDFLAGS environment variables to provides paths for the compiler 49 | and linker to use. For example, with GNU Readline install under /usr/local/opt, 50 | you might use: 51 | 52 | CPPFLAGS="-I/usr/local/opt/readline/include" \ 53 | LDFLAGS="-L/usr/local/opt/readline/lib" \ 54 | ./configure 55 | 56 | If your system uses a different name for Readline, you can set the 57 | $READLINE_LIB environment variable. For example, on OpenBSD where the Readline 58 | library is named ereadline, you might use: 59 | 60 | READLINE_LIB="ereadline" \ 61 | CPPFLAGS="-I/usr/local/include/ereadline" \ 62 | LDFLAGS="-L/usr/local/lib" \ 63 | ./configure 64 | 65 | Finally, on macOS systems with a universal Ruby binary (e.g. /usr/bin/ruby), 66 | and a non-universal Readline shared library, you may need to explicitly pass 67 | an architecture to use via the $READLINE_ARCH environment variable: 68 | 69 | READLINE_ARCH=x86_64 \ 70 | RUBY=/usr/bin/ruby \ 71 | ./configure 72 | 73 | 74 | Installation 75 | ------------ 76 | 77 | 1. Download and extract the latest release: 78 | 79 | curl -OL https://github.com/thoughtbot/gitsh/releases/download/v0.14/gitsh-0.14.tar.gz 80 | tar -zxvf gitsh-0.14.tar.gz 81 | cd gitsh-0.14 82 | 83 | 2. Configure the distribution. This step will determine which version of Ruby 84 | should be used, which has important implications; see the notes on "gitsh and 85 | Ruby version managers" above. 86 | 87 | ./configure 88 | 89 | 3. Build and install gitsh: 90 | 91 | make 92 | sudo make install 93 | -------------------------------------------------------------------------------- /INSTALL.in: -------------------------------------------------------------------------------- 1 | gitsh installation 2 | ================== 3 | 4 | The ideal way to install gitsh is via your operating system's package manager. 5 | Currently gitsh packages are available for: 6 | 7 | * macOS (via Homebrew) 8 | * Arch Linux 9 | * OpenBSD (-current) 10 | 11 | On other operating systems you should install using the tarball, following the 12 | instructions in this guide. 13 | 14 | 15 | Prerequisites 16 | ------------- 17 | 18 | * Ruby version 2.3.0 or later 19 | * gcc or a similar C compiler 20 | * GNU Readline 21 | 22 | 23 | gitsh and Ruby version managers 24 | ------------------------------- 25 | 26 | The gitsh configuration script will attempt to find a system wide version of 27 | Ruby 2.3.0 or later. Rubies installed by Ruby version managers will usually be 28 | ignored to avoid problems when those binaries are moved or deleted. 29 | 30 | To force gitsh to use a specific Ruby binary, set the $RUBY environment variable 31 | when running the configuration script. For example, this will use the first ruby 32 | binary on the $PATH: 33 | 34 | RUBY=$(which ruby) ./configure 35 | 36 | 37 | GNU Readline 38 | ------------ 39 | 40 | gitsh provides a Ruby C extension that integrates with GNU Readline. Ruby ships 41 | with its own Readline module, but it doesn't offer all of the features required 42 | for comprehensive tab-completion. 43 | 44 | Other line editor libraries, like libedit, are partly compatible with GNU 45 | Readline, but don't offer enough features to be supported by gitsh. 46 | 47 | The configure script may fail if GNU Readline isn't found. You can set the 48 | $CPPFLAGS and $LDFLAGS environment variables to provides paths for the compiler 49 | and linker to use. For example, with GNU Readline install under /usr/local/opt, 50 | you might use: 51 | 52 | CPPFLAGS="-I/usr/local/opt/readline/include" \ 53 | LDFLAGS="-L/usr/local/opt/readline/lib" \ 54 | ./configure 55 | 56 | If your system uses a different name for Readline, you can set the 57 | $READLINE_LIB environment variable. For example, on OpenBSD where the Readline 58 | library is named ereadline, you might use: 59 | 60 | READLINE_LIB="ereadline" \ 61 | CPPFLAGS="-I/usr/local/include/ereadline" \ 62 | LDFLAGS="-L/usr/local/lib" \ 63 | ./configure 64 | 65 | Finally, on macOS systems with a universal Ruby binary (e.g. /usr/bin/ruby), 66 | and a non-universal Readline shared library, you may need to explicitly pass 67 | an architecture to use via the $READLINE_ARCH environment variable: 68 | 69 | READLINE_ARCH=x86_64 \ 70 | RUBY=/usr/bin/ruby \ 71 | ./configure 72 | 73 | 74 | Installation 75 | ------------ 76 | 77 | 1. Download and extract the latest release: 78 | 79 | curl -OL https://github.com/thoughtbot/gitsh/releases/download/v@PACKAGE_VERSION@/gitsh-@PACKAGE_VERSION@.tar.gz 80 | tar -zxvf gitsh-@PACKAGE_VERSION@.tar.gz 81 | cd gitsh-@PACKAGE_VERSION@ 82 | 83 | 2. Configure the distribution. This step will determine which version of Ruby 84 | should be used, which has important implications; see the notes on "gitsh and 85 | Ruby version managers" above. 86 | 87 | ./configure 88 | 89 | 3. Build and install gitsh: 90 | 91 | make 92 | sudo make install 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mike Burns, George Brocklehurst, & thoughtbot 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /arch/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Pat Brisbin 2 | pkgname='gitsh' 3 | pkgver=0.14 4 | pkgrel=1 5 | pkgdesc='An interactive shell for git' 6 | arch=('any') 7 | url="http://thoughtbot.github.io/gitsh/" 8 | license=('custom') 9 | depends=('ruby>=2.3.0' 'readline') 10 | source=("http://thoughtbot.github.io/gitsh/gitsh-0.14.tar.gz") 11 | sha256sums=('4b89b6d006326a7b57c4c8e440594e477db61b7d3fe2633a8aad176bb19d0125') 12 | 13 | build() { 14 | cd "$srcdir/$pkgname-$pkgver" 15 | 16 | ./configure \ 17 | --disable-debug \ 18 | --disable-dependency-tracking \ 19 | --disable-silent-rules \ 20 | --prefix=/usr 21 | 22 | make 23 | } 24 | 25 | package() { 26 | cd "$srcdir/$pkgname-$pkgver" 27 | 28 | make DESTDIR="$pkgdir/" install 29 | 30 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/${pkgname}/LICENSE" 31 | } 32 | 33 | # vim:set ts=2 sw=2 et: 34 | -------------------------------------------------------------------------------- /arch/PKGBUILD.in: -------------------------------------------------------------------------------- 1 | # Maintainer: Pat Brisbin 2 | pkgname='@PACKAGE@' 3 | pkgver=@PACKAGE_VERSION@ 4 | pkgrel=1 5 | pkgdesc='An interactive shell for git' 6 | arch=('any') 7 | url="http://thoughtbot.github.io/@PACKAGE@/" 8 | license=('custom') 9 | depends=('ruby>=2.3.0' 'readline') 10 | source=("http://thoughtbot.github.io/@PACKAGE@/@DIST_ARCHIVES@") 11 | sha256sums=('@DIST_SHA@') 12 | 13 | build() { 14 | cd "$srcdir/$pkgname-$pkgver" 15 | 16 | ./configure \ 17 | --disable-debug \ 18 | --disable-dependency-tracking \ 19 | --disable-silent-rules \ 20 | --prefix=/usr 21 | 22 | make 23 | } 24 | 25 | package() { 26 | cd "$srcdir/$pkgname-$pkgver" 27 | 28 | make DESTDIR="$pkgdir/" install 29 | 30 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/${pkgname}/LICENSE" 31 | } 32 | 33 | # vim:set ts=2 sw=2 et: 34 | -------------------------------------------------------------------------------- /assets/gitsh-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/gitsh/7b890ea6314cfc08a25f5a2c94322e5368a7ffa5/assets/gitsh-logo.png -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | aclocal -I m4 && autoconf && automake --add-missing 4 | -------------------------------------------------------------------------------- /bin/gitsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This bin file is provided for convenience during development, the 4 | # distribution binary is built from the template at src/gitsh.rb.in and is 5 | # executed by the C binary built from src/gitsh.c 6 | 7 | require 'bundler/setup' 8 | %w( lib ext ).each do |directory| 9 | $LOAD_PATH.unshift(File.expand_path("../../#{directory}", __FILE__)) 10 | end 11 | 12 | require 'gitsh/environment' 13 | require 'gitsh/cli' 14 | 15 | env = Gitsh::Environment.new( 16 | config_directory: File.expand_path('../../etc', __FILE__), 17 | ) 18 | Gitsh::CLI.new(env: env).run 19 | -------------------------------------------------------------------------------- /bin/tcviz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | %w( lib ext ).each do |directory| 5 | $LOAD_PATH.unshift(File.expand_path("../../#{directory}", __FILE__)) 6 | end 7 | 8 | require 'gitsh/environment' 9 | require 'gitsh/tab_completion/automaton_factory' 10 | require 'gitsh/tab_completion/visualization' 11 | 12 | env = Gitsh::Environment.new( 13 | config_directory: File.expand_path('../../etc', __FILE__), 14 | ) 15 | automaton = Gitsh::TabCompletion::AutomatonFactory.build(env) 16 | viz = Gitsh::TabCompletion::Visualization.new(automaton) 17 | 18 | $stdout.puts viz.to_dot 19 | $stderr.puts viz.summary 20 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_PREREQ([2.68]) 5 | AC_INIT(gitsh, 0.14, hello@thoughtbot.com) 6 | AM_INIT_AUTOMAKE([subdir-objects]) 7 | 8 | AC_ARG_VAR([RUBY],[The path of the Ruby binary to use]) 9 | AC_ARG_VAR( 10 | [READLINE_LIB], 11 | [The name of the readline library on your system, e.g. ereadline on OpenBSD] 12 | ) 13 | AC_ARG_VAR( 14 | [READLINE_ARCH], 15 | [The architecture of the readline library on your system, e.g. x86_64] 16 | ) 17 | 18 | AC_CONFIG_MACRO_DIR([m4]) 19 | 20 | AC_PROG_CC 21 | AC_PATH_PROGS([RUBY], [ruby ruby23 ruby24 ruby25 ruby26], [], $(getconf PATH)) 22 | AC_PATH_PROGS([RUBY], [ruby ruby23 ruby24 ruby25 ruby26]) 23 | 24 | if test -n $RUBY; then 25 | case $RUBY in $HOME/*) 26 | AC_MSG_WARN([Using a non-system Ruby. Disable rvm, rbenv, etc. or set \$RUBY]) 27 | esac 28 | fi 29 | 30 | AX_PROG_RUBY_VERSION( 31 | [2.3.0], 32 | [], 33 | AC_MSG_ERROR(Ruby 2.3.0 or later is required to install gitsh) 34 | ) 35 | 36 | RUBY_CFLAGS=$($RUBY ./configure.rb '-I$(rubyarchhdrdir) -I$(rubyhdrdir)') 37 | RUBY_LIBS=$($RUBY ./configure.rb '-L$($(libdirname)) $(LIBRUBYARG) $(LIBS)') 38 | RUBY_LDADD=$($RUBY ./configure.rb '$($(libdirname))/$(LIBRUBY)') 39 | AC_SUBST([RUBY_CFLAGS]) 40 | AC_SUBST([RUBY_LIBS]) 41 | AC_SUBST([RUBY_LDADD]) 42 | 43 | VENDOR_DIRECTORY="vendor/gems" 44 | 45 | test -d $VENDOR_DIRECTORY || mkdir -p $VENDOR_DIRECTORY 46 | newer=$(ls -t $srcdir/Gemfile.lock $VENDOR_DIRECTORY/setup.rb 2>/dev/null | (read n; echo $n)) 47 | if test "$newer" == "$srcdir/Gemfile.lock"; then 48 | rm -rf $VENDOR_DIRECTORY 49 | $srcdir/vendor/vendorize $VENDOR_DIRECTORY || AC_MSG_ERROR([Vendorizing gems failed]) 50 | fi 51 | 52 | AS_IF([test "x$READLINE_LIB" = x], [READLINE_LIB="readline"]) 53 | AS_IF( 54 | [test "x$READLINE_ARCH" = x], 55 | [readline_arch_arg=""], 56 | [readline_arch_arg="--with-arch-flag=\"-arch $READLINE_ARCH\""] 57 | ) 58 | current_dir="$PWD" 59 | cd "$srcdir/ext/gitsh" 60 | AS_IF( 61 | [$RUBY extconf.rb --with-ldflags="$LDFLAGS" --with-cppflags="$CPPFLAGS" \ 62 | --with-readlinelib="$READLINE_LIB" "$readline_arch_arg"], 63 | [], 64 | AC_MSG_ERROR(Failed to configure Ruby extension) 65 | ) 66 | cd "$current_dir" 67 | 68 | rubydir=$datadir/$PACKAGE/ruby 69 | rubylibdir=$rubydir/lib 70 | pkgrubylibdir=$rubylibdir/gitsh 71 | pkgsysconfdir=$sysconfdir/gitsh 72 | libfiles="$(echo $(find "$srcdir/lib/gitsh" -name \*.rb))" 73 | vendorfiles="$(echo $(find "$srcdir/vendor/gems" -type f))" 74 | testfiles="$(echo $(find "$srcdir/spec/integration" "$srcdir/spec/units" -type f -name \*rb))" 75 | gemsetuppath=$datadir/$PACKAGE/vendor/gems/setup.rb 76 | 77 | AC_SUBST([RUBY]) 78 | AC_SUBST([rubydir]) 79 | AC_SUBST([rubylibdir]) 80 | AC_SUBST([pkgrubylibdir]) 81 | AC_SUBST([pkgsysconfdir]) 82 | AC_SUBST([libfiles]) 83 | AC_SUBST([vendorfiles]) 84 | AC_SUBST([testfiles]) 85 | AC_SUBST([gemsetuppath]) 86 | 87 | AC_CONFIG_FILES([Makefile INSTALL]) 88 | AC_OUTPUT 89 | -------------------------------------------------------------------------------- /configure.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | VAR_PATTERN = /\$\((?[a-z_]+)\)/i 4 | 5 | REPLACEMENTS = Hash[RbConfig::MAKEFILE_CONFIG.map do |key, value| 6 | ["$(#{key})", value] 7 | end] 8 | 9 | def expand_config(path) 10 | if path =~ VAR_PATTERN 11 | new_path = path.gsub(VAR_PATTERN, REPLACEMENTS) 12 | expand_config(new_path) 13 | else 14 | path 15 | end 16 | end 17 | 18 | puts expand_config(ARGV[0]) 19 | -------------------------------------------------------------------------------- /homebrew/gitsh.rb.in: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class Gitsh < Formula 4 | SYSTEM_RUBY_PATH = '/usr/bin/ruby' 5 | HOMEBREW_RUBY_PATH = "#{HOMEBREW_PREFIX}/bin/ruby" 6 | 7 | env :std 8 | homepage 'https://github.com/thoughtbot/@PACKAGE@/' 9 | url 'https://thoughtbot.github.io/@PACKAGE@/@DIST_ARCHIVES@' 10 | sha256 '@DIST_SHA@' 11 | 12 | def self.old_system_ruby? 13 | system_ruby_version = `#{SYSTEM_RUBY_PATH} -e "puts RUBY_VERSION"`.chomp 14 | system_ruby_version < '2.3.0' 15 | end 16 | 17 | if old_system_ruby? 18 | depends_on 'Ruby' 19 | end 20 | depends_on 'readline' 21 | 22 | def install 23 | set_ruby_path 24 | set_architecture 25 | system "./configure", "--disable-debug", 26 | "--disable-dependency-tracking", 27 | "--disable-silent-rules", 28 | "--prefix=#{prefix}" 29 | system "make", "install" 30 | end 31 | 32 | test do 33 | system "#{bin}/gitsh", "--version" 34 | end 35 | 36 | private 37 | 38 | def set_ruby_path 39 | if self.class.old_system_ruby? || File.exist?(HOMEBREW_RUBY_PATH) 40 | ENV['RUBY'] = HOMEBREW_RUBY_PATH 41 | else 42 | ENV['RUBY'] = SYSTEM_RUBY_PATH 43 | end 44 | end 45 | 46 | def set_architecture 47 | ENV['READLINE_ARCH'] = "-arch #{MacOS.preferred_arch}" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/gitsh/argument_list.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class ArgumentList 3 | def initialize(args) 4 | @args = args 5 | end 6 | 7 | def length 8 | args.length 9 | end 10 | 11 | def values(env) 12 | @args.map do |arg| 13 | arg.value(env) 14 | end 15 | end 16 | 17 | private 18 | 19 | attr_reader :args 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitsh/arguments/composite_argument.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module Arguments 3 | class CompositeArgument 4 | def initialize(parts) 5 | @parts = parts 6 | end 7 | 8 | def value(env) 9 | parts.map { |part| part.value(env) }.join('') 10 | end 11 | 12 | def ==(other) 13 | other.is_a?(self.class) && parts == other.parts 14 | end 15 | 16 | protected 17 | 18 | attr_reader :parts 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitsh/arguments/string_argument.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module Arguments 3 | class StringArgument 4 | def initialize(value) 5 | @raw_value = value 6 | end 7 | 8 | def value(_env) 9 | raw_value 10 | end 11 | 12 | def ==(other) 13 | other.is_a?(self.class) && raw_value == other.raw_value 14 | end 15 | 16 | protected 17 | 18 | attr_reader :raw_value 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitsh/arguments/subshell.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/capturing_environment' 2 | 3 | module Gitsh 4 | module Arguments 5 | class Subshell 6 | def initialize(command, options = {}) 7 | @command = command 8 | end 9 | 10 | def value(env) 11 | capturing_env = CapturingEnvironment.new(env.clone) 12 | command.execute(capturing_env) 13 | strip_whitespace(capturing_env.captured_output) 14 | end 15 | 16 | def ==(other) 17 | other.is_a?(self.class) && command == other.command 18 | end 19 | 20 | protected 21 | 22 | attr_reader :command 23 | 24 | private 25 | 26 | def strip_whitespace(output) 27 | output. 28 | sub(%r{\r?\n\Z}, ''). 29 | gsub(%r{\s+}, ' ') 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gitsh/arguments/variable_argument.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module Arguments 3 | class VariableArgument 4 | def initialize(variable_name) 5 | @variable_name = variable_name 6 | end 7 | 8 | def value(env) 9 | env.fetch(variable_name) 10 | end 11 | 12 | def ==(other) 13 | other.is_a?(self.class) && variable_name == other.variable_name 14 | end 15 | 16 | protected 17 | 18 | attr_reader :variable_name 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitsh/capturing_environment.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module Gitsh 4 | class CapturingEnvironment < SimpleDelegator 5 | def initialize(env) 6 | super 7 | @reader, @writer = IO.pipe 8 | end 9 | 10 | def output_stream 11 | writer 12 | end 13 | 14 | def print(*args) 15 | output_stream.print(*args) 16 | end 17 | 18 | def puts(*args) 19 | output_stream.puts(*args) 20 | end 21 | 22 | def captured_output 23 | writer.close 24 | reader.read 25 | end 26 | 27 | private 28 | 29 | attr_reader :reader, :writer 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitsh/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'gitsh/environment' 3 | require 'gitsh/exit_statuses' 4 | require 'gitsh/input_strategies/file' 5 | require 'gitsh/input_strategies/interactive' 6 | require 'gitsh/interpreter' 7 | require 'gitsh/version' 8 | 9 | module Gitsh 10 | class CLI 11 | def initialize(opts={}) 12 | @env = opts.fetch(:env, Environment.new) 13 | @unparsed_args = opts.fetch(:args, ARGV).clone 14 | @interactive_input_strategy = opts.fetch(:interactive_input_strategy) do 15 | InputStrategies::Interactive.new(env: @env) 16 | end 17 | end 18 | 19 | def run 20 | parse_arguments 21 | ensure_executable_git 22 | interpreter.run 23 | rescue NoInputError => error 24 | env.puts_error("gitsh: #{error.message}") 25 | exit EX_NOINPUT 26 | end 27 | 28 | private 29 | 30 | attr_reader :env, :unparsed_args, :script_file_argument, 31 | :interactive_input_strategy 32 | 33 | def interpreter 34 | Interpreter.new(env: env, input_strategy: input_strategy) 35 | end 36 | 37 | def input_strategy 38 | if script_file 39 | InputStrategies::File.new(env: env, path: script_file) 40 | else 41 | interactive_input_strategy 42 | end 43 | end 44 | 45 | def script_file 46 | if script_file_argument 47 | script_file_argument 48 | elsif !env.tty? 49 | InputStrategies::File::STDIN_PLACEHOLDER 50 | end 51 | end 52 | 53 | def parse_arguments 54 | option_parser.parse!(unparsed_args) 55 | @script_file_argument = unparsed_args.pop 56 | 57 | if unparsed_args.any? 58 | exit_with_usage_message 59 | end 60 | rescue OptionParser::InvalidOption 61 | exit_with_usage_message 62 | end 63 | 64 | def exit_with_usage_message 65 | env.puts_error option_parser.banner 66 | exit EX_USAGE 67 | end 68 | 69 | def option_parser 70 | OptionParser.new do |opts| 71 | opts.banner = 'usage: gitsh [--version] [-h | --help] [--git PATH] [script]' 72 | 73 | opts.on('--git PATH', 'Use the specified git command') do |git_command| 74 | env.git_command = git_command 75 | end 76 | 77 | opts.on_tail('--version', 'Display the version and exit') do 78 | env.puts VERSION 79 | exit EX_OK 80 | end 81 | 82 | opts.on_tail('--help', '-h', 'Display this help message and exit') do 83 | env.puts opts 84 | exit EX_OK 85 | end 86 | end 87 | end 88 | 89 | def ensure_executable_git 90 | IO.popen(env.git_command).close 91 | rescue Errno::ENOENT 92 | env.puts_error( 93 | "gitsh: #{env.git_command}: No such file or directory\nEnsure git is "\ 94 | 'on your PATH, or specify the path to git using the --git option', 95 | ) 96 | exit EX_UNAVAILABLE 97 | rescue Errno::EACCES 98 | env.puts_error( 99 | "gitsh: #{env.git_command}: Permission denied\nEnsure git is "\ 100 | 'executable', 101 | ) 102 | exit EX_UNAVAILABLE 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/gitsh/colors.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module Colors 3 | NONE = '' 4 | CLEAR = "\033[00m" 5 | BLACK_FG = "\033[30m" 6 | RED_FG = "\033[31m" 7 | GREEN_FG = "\033[32m" 8 | YELLOW_FG = "\033[33m" 9 | BLUE_FG = "\033[34m" 10 | MAGENTA_FG = "\033[35m" 11 | CYAN_FG = "\033[36m" 12 | WHITE_FG = "\033[37m" 13 | RED_BG = "\033[41m" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/gitsh/commands/git_command.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | require 'gitsh/shell_command_runner' 3 | 4 | module Gitsh 5 | module Commands 6 | class GitCommand 7 | def initialize(command, arg_values, options = {}) 8 | @command = command 9 | @arg_values = arg_values 10 | @shell_command_runner = options.fetch( 11 | :shell_command_runner, 12 | ShellCommandRunner, 13 | ) 14 | end 15 | 16 | def execute(env) 17 | shell_command_runner.run(command_with_arguments(env), env) 18 | end 19 | 20 | private 21 | 22 | attr_reader :command, :arg_values, :shell_command_runner 23 | 24 | def command_with_arguments(env) 25 | if autocorrect_enabled?(env) && command == 'git' 26 | [git_command(env), config_arguments(env), arg_values].flatten 27 | else 28 | [git_command(env), config_arguments(env), command, arg_values].flatten 29 | end 30 | end 31 | 32 | def git_command(env) 33 | Shellwords.split(env.git_command) 34 | end 35 | 36 | def config_arguments(env) 37 | env.config_variables.map { |k, v| ['-c', "#{k}=#{v}"] } 38 | end 39 | 40 | def autocorrect_enabled?(env) 41 | env.fetch('help.autocorrect') { '0' } != '0' 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/gitsh/commands/lazy_command.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/argument_list' 2 | require 'gitsh/commands/git_command' 3 | require 'gitsh/commands/internal_command' 4 | require 'gitsh/commands/shell_command' 5 | 6 | module Gitsh 7 | module Commands 8 | class LazyCommand 9 | COMMAND_PREFIX_MATCHER = /^([:!])?(.+)$/ 10 | COMMAND_CLASS_BY_PREFIX = { 11 | nil => Gitsh::Commands::GitCommand, 12 | ':' => Gitsh::Commands::InternalCommand, 13 | '!' => Gitsh::Commands::ShellCommand, 14 | }.freeze 15 | 16 | def initialize(args = []) 17 | @args = args.compact 18 | end 19 | 20 | def execute(env) 21 | arg_values = argument_list.values(env) 22 | prefix, command = split_command(arg_values.shift) 23 | command_class(prefix).new(command, arg_values).execute(env) 24 | rescue Gitsh::Error => error 25 | env.puts_error("gitsh: #{error.message}") 26 | false 27 | end 28 | 29 | private 30 | 31 | attr_reader :args 32 | 33 | def command_class(prefix) 34 | COMMAND_CLASS_BY_PREFIX.fetch(prefix) 35 | end 36 | 37 | def split_command(command) 38 | COMMAND_PREFIX_MATCHER.match(command).values_at(1, 2) 39 | end 40 | 41 | def argument_list 42 | ArgumentList.new(args) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/gitsh/commands/noop.rb: -------------------------------------------------------------------------------- 1 | module Gitsh::Commands 2 | class Noop 3 | def execute(_env) 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/gitsh/commands/shell_command.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/shell_command_runner' 2 | 3 | module Gitsh 4 | module Commands 5 | class ShellCommand 6 | SHELLWORDS_WHITELIST = 'A-Za-z0-9_\-.,:\/@\n'.freeze 7 | GLOB_WHITELIST = '\*\[\]!\?\\\\'.freeze 8 | SHELL_CHARACTER_FILTER = /([^#{SHELLWORDS_WHITELIST}#{GLOB_WHITELIST}])/ 9 | 10 | def initialize(command, arg_values, options = {}) 11 | @command = command 12 | @arg_values = arg_values 13 | @shell_command_runner = options.fetch( 14 | :shell_command_runner, 15 | ShellCommandRunner, 16 | ) 17 | end 18 | 19 | def execute(env) 20 | shell_command_runner.run(command_with_arguments, env) 21 | end 22 | 23 | private 24 | 25 | attr_reader :command, :arg_values, :shell_command_runner 26 | 27 | def command_with_arguments 28 | [ 29 | '/bin/sh', 30 | '-c', 31 | [command, escaped_arg_values].flatten.join(' '), 32 | ] 33 | end 34 | 35 | def escaped_arg_values 36 | arg_values.map do |arg| 37 | arg.gsub(SHELL_CHARACTER_FILTER, '\\\\\\1') 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/gitsh/commands/tree.rb: -------------------------------------------------------------------------------- 1 | module Gitsh::Commands 2 | module Tree 3 | class Branch 4 | def initialize(left, right) 5 | @left = left 6 | @right = right 7 | end 8 | 9 | private 10 | 11 | attr_reader :left, :right 12 | end 13 | 14 | class Multi < Branch 15 | def execute(env) 16 | left.execute(env) 17 | right.execute(env) 18 | end 19 | end 20 | 21 | class Or < Branch 22 | def execute(env) 23 | left.execute(env) || right.execute(env) 24 | end 25 | end 26 | 27 | class And < Branch 28 | def execute(env) 29 | left.execute(env) && right.execute(env) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gitsh/error.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class Error < StandardError 3 | end 4 | 5 | class UnsetVariableError < Error 6 | end 7 | 8 | class NoInputError < Error 9 | end 10 | 11 | class ParseError < Error 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gitsh/exit_statuses.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | EX_OK = 0 3 | EX_USAGE = 64 4 | EX_NOINPUT = 66 5 | EX_UNAVAILABLE = 69 6 | end 7 | -------------------------------------------------------------------------------- /lib/gitsh/file_runner.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/interpreter' 2 | require 'gitsh/input_strategies/file' 3 | 4 | module Gitsh 5 | class FileRunner 6 | def self.run(opts) 7 | new(opts).run 8 | end 9 | 10 | def initialize(opts) 11 | @env = opts.fetch(:env) 12 | @path = opts.fetch(:path) 13 | end 14 | 15 | def run 16 | interpreter.run 17 | end 18 | 19 | private 20 | 21 | attr_reader :env, :path 22 | 23 | def interpreter 24 | Interpreter.new(env: env, input_strategy: input_strategy) 25 | end 26 | 27 | def input_strategy 28 | InputStrategies::File.new(env: env, path: path) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitsh/git_command_list.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class GitCommandList 3 | def initialize(env) 4 | @env = env 5 | end 6 | 7 | def to_a 8 | try_using(commands_from_list_cmds) do 9 | try_using(commands_from_help('help -a --no-verbose')) do 10 | try_using(commands_from_help('help -a')) 11 | end 12 | end 13 | end 14 | 15 | private 16 | 17 | attr_accessor :env 18 | 19 | def try_using(result, default: []) 20 | if result && result.any? 21 | result 22 | elsif block_given? 23 | yield 24 | else 25 | default 26 | end 27 | end 28 | 29 | def commands_from_list_cmds 30 | git_output('--list-cmds=main,nohelpers').sort 31 | end 32 | 33 | def commands_from_help(command) 34 | git_output(command). 35 | select { |line| line =~ /^ [a-z]/ }. 36 | map { |line| line.split(/\s+/) }. 37 | flatten. 38 | reject { |cmd| cmd.empty? || cmd =~ /--/ }. 39 | sort 40 | end 41 | 42 | def git_output(command) 43 | output, _, status = Open3.capture3(git_command(command)) 44 | 45 | if status.success? 46 | output.chomp.lines.map(&:chomp) 47 | else 48 | [] 49 | end 50 | end 51 | 52 | def git_command(sub_command) 53 | "#{env.git_command} #{sub_command}" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/gitsh/git_repository/status.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class GitRepository 3 | class Status 4 | def initialize(status_porcelain, git_dir) 5 | @status_porcelain = status_porcelain 6 | @git_dir = git_dir 7 | @initialized = File.exist?(git_dir) 8 | end 9 | 10 | def initialized? 11 | @initialized 12 | end 13 | 14 | def has_untracked_files? 15 | status_porcelain.lines.select { |l| l.start_with?('??') }.any? 16 | end 17 | 18 | def has_modified_files? 19 | status_porcelain.lines.select { |l| l =~ /^ ?[A-Z]/ }.any? 20 | end 21 | 22 | private 23 | 24 | attr_reader :status_porcelain, :git_dir 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gitsh/history.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class History 3 | DEFAULT_HISTORY_FILE = "#{Dir.home}/.gitsh_history" 4 | DEFAULT_HISTORY_SIZE = 500 5 | 6 | def initialize(env, line_editor) 7 | @env = env 8 | @line_editor = line_editor 9 | end 10 | 11 | def load 12 | File.read(history_file_path).lines.each do |command| 13 | line_editor::HISTORY << command.chomp 14 | end 15 | rescue Errno::ENOENT 16 | end 17 | 18 | def save 19 | File.open(history_file_path, 'w') do |file| 20 | line_editor::HISTORY.to_a.last(history_size).each do |command| 21 | file << "#{command}\n" 22 | end 23 | end 24 | end 25 | 26 | private 27 | 28 | attr_reader :env, :line_editor 29 | 30 | def history_file_exists? 31 | File.exist?(history_file_path) 32 | end 33 | 34 | def history_file_path 35 | File.expand_path(env.fetch('gitsh.historyFile') { DEFAULT_HISTORY_FILE }) 36 | end 37 | 38 | def history_size 39 | env.fetch('gitsh.historySize') { DEFAULT_HISTORY_SIZE }.to_i 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/gitsh/input_strategies/file.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/error' 2 | 3 | module Gitsh 4 | module InputStrategies 5 | class File 6 | STDIN_PLACEHOLDER = '-'.freeze 7 | 8 | def initialize(opts) 9 | @env = opts[:env] 10 | @path = opts.fetch(:path) 11 | end 12 | 13 | def setup 14 | @file = open_file 15 | rescue Errno::ENOENT 16 | raise NoInputError, "#{path}: No such file or directory" 17 | rescue Errno::EACCES 18 | raise NoInputError, "#{path}: Permission denied" 19 | end 20 | 21 | def teardown 22 | if file 23 | file.close 24 | end 25 | end 26 | 27 | def read_command 28 | next_line 29 | rescue EOFError 30 | nil 31 | end 32 | 33 | def read_continuation 34 | next_line 35 | end 36 | 37 | def handle_parse_error(message) 38 | raise ParseError, message 39 | end 40 | 41 | private 42 | 43 | attr_reader :env, :file, :path 44 | 45 | def open_file 46 | if path == STDIN_PLACEHOLDER 47 | env.input_stream 48 | else 49 | ::File.open(path) 50 | end 51 | end 52 | 53 | def next_line 54 | file.readline.chomp 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/gitsh/interpreter.rb: -------------------------------------------------------------------------------- 1 | require 'rltk' 2 | require 'gitsh/commands/noop' 3 | require 'gitsh/error' 4 | require 'gitsh/lexer' 5 | require 'gitsh/parser' 6 | 7 | module Gitsh 8 | class Interpreter 9 | def initialize(options) 10 | @env = options.fetch(:env) 11 | @lexer = options.fetch(:lexer, Lexer) 12 | @parser = options.fetch(:parser, Parser) 13 | @input_strategy = options.fetch(:input_strategy) 14 | end 15 | 16 | def run 17 | input_strategy.setup 18 | while command = input_strategy.read_command 19 | execute(command) 20 | end 21 | ensure 22 | input_strategy.teardown 23 | end 24 | 25 | private 26 | 27 | attr_reader :env, :parser, :lexer, :input_strategy 28 | 29 | def execute(input) 30 | build_command(input).execute(env) 31 | rescue RLTK::LexingError, RLTK::NotInLanguage, RLTK::BadToken, EOFError 32 | input_strategy.handle_parse_error('parse error') 33 | end 34 | 35 | def build_command(input) 36 | tokens = lexer.lex(input) 37 | 38 | if incomplete_command?(tokens) 39 | continuation = input_strategy.read_continuation 40 | build_multi_line_command(input, continuation) 41 | else 42 | parser.parse(tokens) 43 | end 44 | end 45 | 46 | def incomplete_command?(tokens) 47 | tokens.reverse_each.detect { |token| token.type == :INCOMPLETE } 48 | end 49 | 50 | def build_multi_line_command(previous_lines, new_line) 51 | if new_line.nil? 52 | Commands::Noop.new 53 | else 54 | build_command([previous_lines, new_line].join("\n")) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/gitsh/lexer/character_class.rb: -------------------------------------------------------------------------------- 1 | require 'rltk' 2 | 3 | module Gitsh 4 | class Lexer < RLTK::Lexer 5 | class CharacterClass 6 | attr_reader :characters 7 | 8 | def initialize(characters) 9 | @characters = characters 10 | end 11 | 12 | def +(other) 13 | self.class.new(characters + other.characters) 14 | end 15 | 16 | def to_regexp 17 | Regexp.new("[#{Regexp.escape(characters.join)}]") 18 | end 19 | 20 | def to_negative_regexp 21 | Regexp.new("[^#{Regexp.escape(characters.join)}]") 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/line_editor.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/line_editor_native' 2 | 3 | module Gitsh 4 | module LineEditor 5 | def self.delete_text(*args) 6 | return if line_buffer.nil? 7 | 8 | if args.length.zero? 9 | beg = 0 10 | len = line_buffer.length 11 | elsif args.length == 1 && args[0].is_a?(Range) 12 | beg = args[0].begin 13 | len = line_buffer[args[0]].length 14 | elsif args.length == 1 15 | beg = args[0] 16 | len = line_buffer.length - beg 17 | elsif args.length == 2 18 | beg = args[0] 19 | len = args[1] 20 | else 21 | raise ArgumentError, 22 | "wrong number of arguments (given #{args.length}, expected 0..2)" 23 | end 24 | 25 | byte_beg = line_buffer[0...beg].bytesize 26 | byte_len = line_buffer[beg, len].bytesize 27 | 28 | self.delete_bytes(byte_beg, byte_len) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitsh/line_editor_history_filter.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/module_delegator' 2 | 3 | module Gitsh 4 | class LineEditorHistoryFilter < ModuleDelegator 5 | def readline(prompt, add_hist = false) 6 | module_delegator_target.readline(prompt, add_hist).tap do |input| 7 | if add_hist && input && should_not_have_been_added_to_history? 8 | history.pop 9 | end 10 | end 11 | end 12 | 13 | private 14 | 15 | def should_not_have_been_added_to_history? 16 | empty? || duplicate? 17 | end 18 | 19 | def empty? 20 | history[-1].empty? 21 | end 22 | 23 | def duplicate? 24 | history.length > 1 && history[-1] == history[-2] 25 | end 26 | 27 | def history 28 | module_delegator_target::HISTORY 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitsh/magic_variables.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class MagicVariables 3 | def initialize(repo) 4 | @repo = repo 5 | end 6 | 7 | def fetch(key) 8 | if available_variables.include?(key) 9 | send(key) 10 | else 11 | yield 12 | end 13 | end 14 | 15 | def available_variables 16 | private_methods(false).grep(/^_/) 17 | end 18 | 19 | private 20 | 21 | attr_reader :repo 22 | 23 | def _prior 24 | repo.revision_name('@{-1}') || 25 | raise(UnsetVariableError, 'No prior branch') 26 | end 27 | 28 | def _merge_base 29 | repo.merge_base('HEAD', 'MERGE_HEAD').tap do |merge_base| 30 | if merge_base.empty? 31 | raise UnsetVariableError, 'No merge in progress' 32 | end 33 | end 34 | end 35 | 36 | def _rebase_base 37 | read_file(['rebase-apply', 'onto']) || 38 | read_file(['rebase-merge', 'onto']) || 39 | raise(UnsetVariableError, 'No rebase in progress') 40 | end 41 | 42 | def _root 43 | repo.root_dir.tap do |root_dir| 44 | if root_dir.empty? 45 | raise(UnsetVariableError, 'Not a Git repository') 46 | end 47 | end 48 | end 49 | 50 | def read_file(path_components) 51 | File.read(File.join(repo.git_dir, *path_components)).chomp 52 | rescue Errno::ENOENT 53 | nil 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/gitsh/module_delegator.rb: -------------------------------------------------------------------------------- 1 | class ModuleDelegator < Module 2 | def initialize(module_delegator_target) 3 | @module_delegator_target = module_delegator_target 4 | end 5 | 6 | def method_missing(method_name, *args, &block) 7 | module_delegator_target.send(method_name, *args, &block) 8 | end 9 | 10 | def respond_to_missing?(method_name, include_all) 11 | module_delegator_target.respond_to?(method_name, include_all) 12 | end 13 | 14 | def const_missing(const_name) 15 | module_delegator_target.const_get(const_name) 16 | end 17 | 18 | private 19 | 20 | attr_reader :module_delegator_target 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitsh/parser.rb: -------------------------------------------------------------------------------- 1 | require 'rltk' 2 | require 'gitsh/arguments/string_argument' 3 | require 'gitsh/arguments/composite_argument' 4 | require 'gitsh/arguments/variable_argument' 5 | require 'gitsh/arguments/subshell' 6 | require 'gitsh/commands/lazy_command' 7 | require 'gitsh/commands/noop' 8 | require 'gitsh/commands/tree' 9 | 10 | module Gitsh 11 | class Parser < RLTK::Parser 12 | left :EOL 13 | left :SEMICOLON 14 | left :OR 15 | left :AND 16 | 17 | production(:program) do 18 | clause('SPACE? .commands SEMICOLON? SPACE?') { |c| c } 19 | clause('SPACE?') { |_| Commands::Noop.new } 20 | end 21 | 22 | production(:commands) do 23 | clause('command') { |c| c } 24 | clause('LEFT_PAREN .commands RIGHT_PAREN SPACE?') { |c| c } 25 | clause('.commands EOL .commands') { |c1, c2| Commands::Tree::Multi.new(c1, c2) } 26 | clause('.commands SEMICOLON .commands') { |c1, c2| Commands::Tree::Multi.new(c1, c2) } 27 | clause('.commands OR .commands') { |c1, c2| Commands::Tree::Or.new(c1, c2) } 28 | clause('.commands AND .commands') { |c1, c2| Commands::Tree::And.new(c1, c2) } 29 | end 30 | 31 | production(:command, 'argument_list') do |args| 32 | Commands::LazyCommand.new(args) 33 | end 34 | 35 | production(:argument_list) do 36 | clause('.argument') { |arg| [arg] } 37 | clause('.argument_list SPACE .argument') { |list, arg| list + [arg] } 38 | end 39 | 40 | production(:argument) do 41 | clause('argument_part') { |part| part } 42 | clause('argument_part argument') do |part, argument| 43 | Arguments::CompositeArgument.new([part, argument]) 44 | end 45 | end 46 | 47 | production(:argument_part) do 48 | clause(:word) { |word| Arguments::StringArgument.new(word) } 49 | clause(:VAR) { |var| Arguments::VariableArgument.new(var) } 50 | clause(:subshell) { |program| Arguments::Subshell.new(program) } 51 | end 52 | 53 | production(:word, 'WORD+') { |words| words.inject(:+) } 54 | 55 | production(:subshell, 'SUBSHELL_START .program SUBSHELL_END') { |p| p } 56 | 57 | finalize 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/gitsh/prompt_color.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/colors' 2 | 3 | module Gitsh 4 | class PromptColor 5 | def initialize(env) 6 | @env = env 7 | end 8 | 9 | def status_color(status) 10 | if !status.initialized? 11 | env.repo_config_color('gitsh.color.uninitialized', 'normal red') 12 | elsif status.has_untracked_files? 13 | env.repo_config_color('gitsh.color.untracked', 'red') 14 | elsif status.has_modified_files? 15 | env.repo_config_color('gitsh.color.modified', 'yellow') 16 | else 17 | env.repo_config_color('gitsh.color.default', 'blue') 18 | end 19 | end 20 | 21 | private 22 | 23 | attr_reader :env 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/prompter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'gitsh/colors' 4 | require 'gitsh/prompt_color' 5 | 6 | module Gitsh 7 | class Prompter 8 | DEFAULT_FORMAT = "%D %c%B%#%w".freeze 9 | BRANCH_CHAR_LIMIT = 15 10 | 11 | def initialize(options={}) 12 | @env = options.fetch(:env) 13 | @use_color = options.fetch(:color, true) 14 | @prompt_color = options.fetch(:prompt_color) { PromptColor.new(@env) } 15 | @options = options 16 | end 17 | 18 | def prompt 19 | Prompt.new(env, use_color, prompt_color).to_s 20 | end 21 | 22 | private 23 | 24 | attr_reader :env, :use_color, :prompt_color 25 | 26 | class Prompt 27 | def initialize(env, use_color, prompt_color) 28 | @env = env 29 | @use_color = use_color 30 | @prompt_color = prompt_color 31 | end 32 | 33 | def to_s 34 | padded_prompt_format.gsub(/%[bBcdDgGw#]/) do |match| 35 | case match 36 | when "%b" then branch_name 37 | when "%B" then shortened_branch_name 38 | when "%c" then status_color 39 | when "%d" then working_directory 40 | when "%D" then File.basename(working_directory) 41 | when "%g" then git_command 42 | when "%G" then File.basename(git_command) 43 | when "%w" then clear_color 44 | when "%#" then terminator 45 | end 46 | end 47 | end 48 | 49 | private 50 | 51 | attr_reader :env, :prompt_color 52 | 53 | def working_directory 54 | Dir.getwd.sub(/\A#{Dir.home}/, '~') 55 | end 56 | 57 | def padded_prompt_format 58 | "#{prompt_format.chomp} " 59 | end 60 | 61 | def prompt_format 62 | env.fetch('gitsh.prompt') { DEFAULT_FORMAT } 63 | end 64 | 65 | def shortened_branch_name 66 | branch_name[0...BRANCH_CHAR_LIMIT] + ellipsis 67 | end 68 | 69 | def ellipsis 70 | if branch_name.length > BRANCH_CHAR_LIMIT 71 | '…' 72 | else 73 | '' 74 | end 75 | end 76 | 77 | def branch_name 78 | @branch_name ||= if repo_status.initialized? 79 | env.repo_current_head 80 | else 81 | 'uninitialized' 82 | end 83 | end 84 | 85 | def git_command 86 | env.git_command 87 | end 88 | 89 | def terminator 90 | if !repo_status.initialized? 91 | '!!' 92 | elsif repo_status.has_untracked_files? 93 | '!' 94 | elsif repo_status.has_modified_files? 95 | '&' 96 | else 97 | '@' 98 | end 99 | end 100 | 101 | def status_color 102 | if use_color? 103 | prompt_color.status_color(repo_status) 104 | else 105 | Colors::NONE 106 | end 107 | end 108 | 109 | def clear_color 110 | if use_color? 111 | Colors::CLEAR 112 | else 113 | Colors::NONE 114 | end 115 | end 116 | 117 | def use_color? 118 | @use_color 119 | end 120 | 121 | def repo_status 122 | @repo_status ||= env.repo_status 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/gitsh/quote_detector.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class QuoteDetector 3 | def call(text, index) 4 | index > 0 && text[index - 1] == '\\' && !call(text, index - 1) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/gitsh/shell_command_runner.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | class ShellCommandRunner 3 | def self.run(command_with_arguments, env) 4 | new(command_with_arguments, env).run 5 | end 6 | 7 | def initialize(command_with_arguments, env) 8 | @command_with_arguments = command_with_arguments 9 | @env = env 10 | end 11 | 12 | def run 13 | pid = Process.spawn( 14 | *command_with_arguments, 15 | out: env.output_stream.to_i, 16 | err: env.error_stream.to_i 17 | ) 18 | wait_for_process(pid) 19 | $? && $?.success? 20 | rescue SystemCallError => e 21 | env.puts_error e.message 22 | false 23 | end 24 | 25 | private 26 | 27 | attr_reader :command_with_arguments, :env 28 | 29 | def wait_for_process(pid) 30 | Process.wait(pid) 31 | rescue Interrupt 32 | Process.kill('INT', pid) 33 | retry 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/alias_expander.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/error' 2 | require 'gitsh/tab_completion/tokens_to_words' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | class AliasExpander 7 | def initialize(words, env) 8 | @words = words 9 | @env = env 10 | end 11 | 12 | def call 13 | if expandable? 14 | expanded_alias_words + words.drop(1) 15 | else 16 | words 17 | end 18 | end 19 | 20 | private 21 | 22 | attr_reader :words, :env 23 | 24 | def expandable? 25 | !expanded_alias.start_with?('!') 26 | rescue Gitsh::UnsetVariableError 27 | false 28 | end 29 | 30 | def expanded_alias_words 31 | TokensToWords.call(expanded_alias_tokens) 32 | end 33 | 34 | def expanded_alias_tokens 35 | Lexer.lex(expanded_alias) 36 | rescue RLTK::LexingError 37 | [] 38 | end 39 | 40 | def expanded_alias 41 | @_expanded_alias ||= env.fetch("alias.#{words.first}") 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/automaton_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | require 'gitsh/tab_completion/dsl' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | class AutomatonFactory 7 | def self.build(env) 8 | new(env).build 9 | end 10 | 11 | def initialize(env) 12 | @env = env 13 | end 14 | 15 | def build 16 | start_state = Automaton::State.new('start') 17 | config_paths.each do |path| 18 | DSL.load(path, start_state, env) 19 | end 20 | Automaton.new(start_state) 21 | end 22 | 23 | private 24 | 25 | attr_reader :env 26 | 27 | def config_paths 28 | [ 29 | File.join(env.config_directory, 'completions'), 30 | File.join(ENV.fetch('HOME', '/'), '.gitsh_completions'), 31 | ] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/command_completer.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | class CommandCompleter 4 | def initialize(line_editor, prior_words, input, automaton, escaper) 5 | @line_editor = line_editor 6 | @prior_words = prior_words 7 | @input = input 8 | @automaton = automaton 9 | @escaper = escaper 10 | end 11 | 12 | def call 13 | line_editor.completion_append_character = completion_append_character 14 | line_editor.completion_suppress_quote = incomplete_path? 15 | 16 | matches 17 | end 18 | 19 | private 20 | 21 | attr_reader :line_editor, :prior_words, :input, :automaton, :escaper 22 | 23 | def completion_append_character 24 | if incomplete_path? 25 | nil 26 | else 27 | ' ' 28 | end 29 | end 30 | 31 | def incomplete_path? 32 | matches.size == 1 && matches.first.end_with?('/') 33 | end 34 | 35 | def matches 36 | @matches ||= automaton. 37 | completions(prior_words, escaper.unescape(input)). 38 | map { |match| escaper.escape(match) } 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/context.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/lexer' 2 | require 'gitsh/tab_completion/tokens_to_words' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | class Context 7 | COMMAND_SEPARATORS = [ 8 | :AND, :OR, :SEMICOLON, :LEFT_PAREN, :SUBSHELL_START, :EOL, 9 | ].freeze 10 | NOT_MEANINGFUL = [:EOS, :INCOMPLETE].freeze 11 | 12 | def initialize(input) 13 | @input = input 14 | end 15 | 16 | def prior_words 17 | words[0...-1] 18 | end 19 | 20 | def completing_variable? 21 | if meaningful_tokens.any? 22 | [:VAR, :MISSING].include?(meaningful_tokens.last.type) 23 | else 24 | false 25 | end 26 | end 27 | 28 | private 29 | 30 | attr_reader :input 31 | 32 | def words 33 | TokensToWords.call(last_command_tokens) 34 | end 35 | 36 | def last_command_tokens 37 | tokens.reverse_each. 38 | take_while { |token| !COMMAND_SEPARATORS.include?(token.type) }. 39 | reverse 40 | end 41 | 42 | def meaningful_tokens 43 | @_meaningful_tokens ||= tokens.reject do |token| 44 | NOT_MEANINGFUL.include?(token.type) 45 | end 46 | end 47 | 48 | def tokens 49 | @_tokens ||= lex 50 | end 51 | 52 | def lex 53 | Lexer.lex(input) 54 | rescue RLTK::LexingError 55 | [] 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/dsl/lexer' 2 | require 'gitsh/tab_completion/dsl/parser' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | module DSL 7 | def self.load(path, start_state, env) 8 | source = File.read(path) 9 | tokens = Lexer.lex(source, path) 10 | factory = Parser.parse(tokens, gitsh_env: env) 11 | factory.build(start_state) 12 | rescue Errno::ENOENT 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/choice_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module DSL 6 | class ChoiceFactory 7 | attr_reader :choices 8 | 9 | def initialize(choices) 10 | @choices = choices 11 | end 12 | 13 | def build(start_state, options = {}) 14 | end_state = options.fetch(:end_state) do 15 | Automaton::State.new('choice') 16 | end 17 | choices.each do |choice| 18 | choice.build(start_state, options.merge(end_state: end_state)) 19 | end 20 | end_state 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/concatenation_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class ConcatenationFactory 5 | attr_reader :parts 6 | 7 | def initialize(parts) 8 | @parts = parts 9 | end 10 | 11 | def build(start_state, options = {}) 12 | with_optional_end_state(options) do 13 | parts.inject(start_state) do |state, part| 14 | part.build(state, options) 15 | end 16 | end 17 | end 18 | 19 | private 20 | 21 | def with_optional_end_state(options) 22 | end_state = options.delete(:end_state) 23 | 24 | if end_state 25 | last_state = yield 26 | last_state.add_free_transition(end_state) 27 | end_state 28 | else 29 | yield 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/fallback_transition_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module DSL 6 | class FallbackTransitionFactory 7 | attr_reader :matcher 8 | 9 | def initialize(matcher) 10 | @matcher = matcher 11 | end 12 | 13 | def build(start_state, options = {}) 14 | end_state = options.fetch(:end_state) do 15 | Automaton::State.new('fallback') 16 | end 17 | start_state.add_fallback_transition(matcher, end_state) 18 | end_state 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/lexer.rb: -------------------------------------------------------------------------------- 1 | require 'rltk/lexer' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module DSL 6 | class Lexer < RLTK::Lexer 7 | WORD_CHARACTERS = /[^\s*+?|()#\$]/ 8 | 9 | rule(/\$opt/) { :OPT_VAR } 10 | rule(/\$[a-z_]+/) { |t| [:VAR, t[1..-1]] } 11 | 12 | rule(/[a-z_]+::/) { |t| [:MODIFIER, t[0..-3]] } 13 | 14 | rule(/-[A-Za-z0-9]/) { |t| [:OPTION, t] } 15 | rule(/--#{WORD_CHARACTERS}+/) { |t| [:OPTION, t] } 16 | 17 | rule(/#{WORD_CHARACTERS}+/) { |t| [:WORD, t] } 18 | 19 | rule(/\*/) { :STAR } 20 | rule(/\+/) { :PLUS } 21 | rule(/\?/) { :MAYBE } 22 | rule(/\|/) { :OR } 23 | rule(/\(/) { :LEFT_PAREN } 24 | rule(/\)/) { :RIGHT_PAREN } 25 | 26 | rule(/\s*\n\s*\n/) { :BLANK } 27 | rule(/\s*\n\s+/) { :INDENT } 28 | rule(/\s+/) {} 29 | 30 | rule(/#/) { push_state :comment } 31 | rule(/[^\n]+/, :comment) {} 32 | rule(/(?=\n)/, :comment) { pop_state } 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/maybe_operation_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class MaybeOperationFactory 5 | attr_reader :child 6 | 7 | def initialize(child) 8 | @child = child 9 | end 10 | 11 | def build(start_state, options = {}) 12 | end_state = child.build(start_state, options) 13 | start_state.add_free_transition(end_state) 14 | end_state 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/null_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class NullFactory 5 | def build(*_) 6 | end 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/option_transition_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | require 'gitsh/tab_completion/matchers/unknown_option_matcher' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | module DSL 7 | class OptionTransitionFactory 8 | def build(start_state, options = {}) 9 | @start_state = start_state 10 | @options = options 11 | 12 | invoke 13 | end 14 | 15 | private 16 | 17 | attr_reader :start_state, :options 18 | 19 | def invoke 20 | add_transitions_for_known_options 21 | add_transitions_for_unknown_options 22 | end_state 23 | end 24 | 25 | def add_transitions_for_known_options 26 | known_options_factory.build( 27 | start_state, 28 | options.merge(end_state: end_state), 29 | ) 30 | end 31 | 32 | def add_transitions_for_unknown_options 33 | start_state.add_transition( 34 | Matchers::UnknownOptionMatcher.new, 35 | end_state, 36 | ) 37 | end 38 | 39 | def end_state 40 | @end_state ||= options.fetch(:end_state) do 41 | Automaton::State.new('option') 42 | end 43 | end 44 | 45 | def known_options_factory 46 | options.fetch(:known_options_factory) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/parse_error.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/error' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module DSL 6 | class ParseError < Gitsh::Error 7 | TOKEN_TO_DESCRIPTION = { 8 | BLANK: 'blank line', 9 | INDENT: 'indent', 10 | 11 | LEFT_PAREN: 'opening paren', 12 | RIGHT_PAREN: 'closing paren', 13 | 14 | MAYBE: 'operator (?)', 15 | OR: 'operator (|)', 16 | PLUS: 'operator (+)', 17 | STAR: 'operator (*)', 18 | 19 | OPTION: 'option (%{value})', 20 | VAR: 'variable ($%{value})', 21 | OPT_VAR: 'variable ($opt)', 22 | WORD: 'word (%{value})', 23 | }.freeze 24 | 25 | def initialize(reason, token) 26 | @reason = reason 27 | @token = token 28 | end 29 | 30 | def to_s 31 | 'Tab completion configuration error: '\ 32 | "#{reason} #{token_description} at line #{line}, column #{column} "\ 33 | "in file #{path}" 34 | end 35 | 36 | private 37 | 38 | attr_reader :reason, :token 39 | 40 | def token_description 41 | TOKEN_TO_DESCRIPTION.fetch(token.type, token.type) % { 42 | value: token.value, 43 | } 44 | end 45 | 46 | def line 47 | token.position.line_number 48 | end 49 | 50 | def column 51 | token.position.line_offset + 1 52 | end 53 | 54 | def path 55 | token.position.file_name 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/plus_operation_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class PlusOperationFactory 5 | attr_reader :child 6 | 7 | def initialize(child) 8 | @child = child 9 | end 10 | 11 | def build(start_state, options = {}) 12 | end_state = child.build(start_state, options) 13 | end_state.add_free_transition(start_state) 14 | end_state 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/rule_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class RuleFactory 5 | attr_reader :root, :options 6 | 7 | def initialize(root, options) 8 | @root = root 9 | @options = options 10 | end 11 | 12 | def build(start_state) 13 | root.build(start_state, known_options_factory: options) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/rule_set_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class RuleSetFactory 5 | attr_reader :rules 6 | 7 | def initialize(rules) 8 | @rules = rules 9 | end 10 | 11 | def build(start_state) 12 | rules.each { |rule| rule.build(start_state) } 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/star_operation_factory.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module DSL 4 | class StarOperationFactory 5 | attr_reader :child 6 | 7 | def initialize(child) 8 | @child = child 9 | end 10 | 11 | def build(start_state, options = {}) 12 | child.build(start_state, options.merge(end_state: start_state)) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/text_transition_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | require 'gitsh/tab_completion/matchers/text_matcher' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | module DSL 7 | class TextTransitionFactory 8 | attr_reader :word 9 | 10 | def initialize(word) 11 | @word = word 12 | end 13 | 14 | def build(start_state, options = {}) 15 | end_state = options.fetch(:end_state) { Automaton::State.new(word) } 16 | start_state.add_transition(Matchers::TextMatcher.new(word), end_state) 17 | end_state 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/dsl/variable_transition_factory.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/automaton' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module DSL 6 | class VariableTransitionFactory 7 | attr_reader :matcher 8 | 9 | def initialize(matcher) 10 | @matcher = matcher 11 | end 12 | 13 | def build(start_state, options = {}) 14 | end_state = options.fetch(:end_state) do 15 | Automaton::State.new(matcher.name) 16 | end 17 | start_state.add_transition(matcher, end_state) 18 | end_state 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/escaper.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/lexer' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | class Escaper 6 | ESCAPABLES = { 7 | nil => Gitsh::Lexer::UNQUOTED_STRING_ESCAPABLES, 8 | '"' => Gitsh::Lexer::SOFT_STRING_ESCAPABLES, 9 | "'" => Gitsh::Lexer::HARD_STRING_ESCAPABLES, 10 | }.freeze 11 | 12 | def initialize(line_editor) 13 | @line_editor = line_editor 14 | end 15 | 16 | def escape(option) 17 | option.gsub(escapables) { |char| "\\#{char}" } 18 | end 19 | 20 | def unescape(input) 21 | input.gsub(/\\(#{escapables})/, '\1') 22 | end 23 | 24 | private 25 | 26 | attr_reader :line_editor 27 | 28 | def escapables 29 | ESCAPABLES[line_editor.completion_quote_character].to_regexp 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/facade.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/alias_expander' 2 | require 'gitsh/tab_completion/automaton_factory' 3 | require 'gitsh/tab_completion/command_completer' 4 | require 'gitsh/tab_completion/context' 5 | require 'gitsh/tab_completion/escaper' 6 | require 'gitsh/tab_completion/variable_completer' 7 | 8 | module Gitsh 9 | module TabCompletion 10 | class Facade 11 | def initialize(line_editor, env) 12 | @line_editor = line_editor 13 | @env = env 14 | @automaton = AutomatonFactory.build(env) 15 | end 16 | 17 | def call(input) 18 | context = Context.new(line_editor.line_buffer) 19 | if context.completing_variable? 20 | variable_completions(input) 21 | else 22 | command_completions(context, input) 23 | end 24 | end 25 | 26 | private 27 | 28 | attr_reader :line_editor, :env, :automaton 29 | 30 | def command_completions(context, input) 31 | CommandCompleter.new( 32 | line_editor, 33 | AliasExpander.new(context.prior_words, env).call, 34 | input, 35 | automaton, 36 | escaper, 37 | ).call 38 | end 39 | 40 | def variable_completions(input) 41 | VariableCompleter.new(line_editor, input, env).call 42 | end 43 | 44 | def escaper 45 | @escaper ||= Escaper.new(line_editor) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/anything_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class AnythingMatcher < BaseMatcher 7 | def initialize(_env) 8 | end 9 | 10 | def name 11 | 'anything' 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/base_matcher.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | module Matchers 4 | class BaseMatcher 5 | def match?(_) 6 | true 7 | end 8 | 9 | def completions(token) 10 | all_completions.select { |option| option.start_with?(token) } 11 | end 12 | 13 | def eql?(other) 14 | self.class == other.class 15 | end 16 | 17 | def hash 18 | self.class.hash + 1 19 | end 20 | 21 | private 22 | 23 | def all_completions 24 | [] 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/branch_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class BranchMatcher < BaseMatcher 7 | def initialize(env) 8 | @env = env 9 | end 10 | 11 | def name 12 | 'branch' 13 | end 14 | 15 | private 16 | 17 | attr_reader :env 18 | 19 | def all_completions 20 | env.repo_branches 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/command_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | require 'gitsh/commands/internal_command' 3 | 4 | module Gitsh 5 | module TabCompletion 6 | module Matchers 7 | class CommandMatcher < BaseMatcher 8 | def initialize(env, internal_command = Commands::InternalCommand) 9 | @env = env 10 | @internal_command = internal_command 11 | end 12 | 13 | def name 14 | 'command' 15 | end 16 | 17 | private 18 | 19 | attr_reader :env, :internal_command 20 | 21 | def all_completions 22 | env.git_commands + env.git_aliases + internal_command.commands 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/path_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class PathMatcher < BaseMatcher 7 | def initialize(_env) 8 | end 9 | 10 | def completions(token) 11 | prefix = normalize_path(token) 12 | paths(prefix).map { |option| option.sub(prefix, token) } 13 | end 14 | 15 | def name 16 | 'path' 17 | end 18 | 19 | private 20 | 21 | def normalize_path(token) 22 | if token == '' || token.end_with?('/') 23 | File.expand_path(token) + '/' 24 | else 25 | File.expand_path(token) 26 | end 27 | end 28 | 29 | def paths(prefix) 30 | Dir["#{prefix}*"].map do |path| 31 | if File.directory?(path) 32 | path + '/' 33 | else 34 | path 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/remote_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class RemoteMatcher < BaseMatcher 7 | def initialize(env) 8 | @env = env 9 | end 10 | 11 | def name 12 | 'remote' 13 | end 14 | 15 | private 16 | 17 | attr_reader :env 18 | 19 | def all_completions 20 | env.repo_remotes 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/revision_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class RevisionMatcher < BaseMatcher 7 | SEPARATORS = /(?:\.\.+|[:^~\\])/ 8 | 9 | def initialize(env) 10 | @env = env 11 | end 12 | 13 | def name 14 | 'revision' 15 | end 16 | 17 | def completions(token) 18 | prefix, partial_name = split(token) 19 | super(partial_name).map { |option| prefix + option } 20 | end 21 | 22 | private 23 | 24 | attr_reader :env 25 | 26 | def all_completions 27 | env.repo_heads 28 | end 29 | 30 | def split(token) 31 | parts = token.rpartition(SEPARATORS) 32 | [parts[0...-1].join, parts[-1]] 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/tag_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class TagMatcher < BaseMatcher 7 | def initialize(env) 8 | @env = env 9 | end 10 | 11 | def name 12 | 'tag' 13 | end 14 | 15 | private 16 | 17 | attr_reader :env 18 | 19 | def all_completions 20 | env.repo_tags 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/text_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class TextMatcher < BaseMatcher 7 | attr_reader :word 8 | 9 | def initialize(word) 10 | @word = word 11 | end 12 | 13 | def match?(match_word) 14 | word == match_word 15 | end 16 | 17 | def eql?(other) 18 | super(other) && other.word == word 19 | end 20 | 21 | def hash 22 | super + word.hash 23 | end 24 | 25 | private 26 | 27 | def all_completions 28 | [word] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/matchers/unknown_option_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/tab_completion/matchers/base_matcher' 2 | 3 | module Gitsh 4 | module TabCompletion 5 | module Matchers 6 | class UnknownOptionMatcher < BaseMatcher 7 | def match?(word) 8 | word =~ /\A--?[^-]/ 9 | end 10 | 11 | def name 12 | 'opt' 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/tokens_to_words.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | class TokensToWords 4 | def self.call(tokens) 5 | new(tokens).call 6 | end 7 | 8 | def initialize(tokens) 9 | @tokens = tokens 10 | end 11 | 12 | def call 13 | words 14 | end 15 | 16 | private 17 | 18 | attr_reader :tokens 19 | 20 | def words 21 | token_groups.map do |tokens| 22 | tokens.inject('') do |result, token| 23 | case token.type 24 | when :WORD 25 | result + token.value 26 | when :VAR 27 | result + "${#{token.value}}" 28 | else 29 | result 30 | end 31 | end 32 | end 33 | end 34 | 35 | def token_groups 36 | tokens. 37 | chunk { |token| token.type == :SPACE }. 38 | inject([]) do |result, (is_space, token_group)| 39 | if is_space 40 | result 41 | else 42 | [*result, token_group] 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/variable_completer.rb: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | module TabCompletion 3 | class VariableCompleter 4 | def initialize(line_editor, input, env) 5 | @line_editor = line_editor 6 | @input = input 7 | @env = env 8 | end 9 | 10 | def call 11 | line_editor.completion_append_character = completion_append_character 12 | line_editor.completion_suppress_quote = true 13 | 14 | matches 15 | end 16 | 17 | private 18 | 19 | attr_reader :line_editor, :input, :env 20 | 21 | def completion_append_character 22 | if prefix.end_with?('{') 23 | '}' 24 | else 25 | nil 26 | end 27 | end 28 | 29 | def matches 30 | env.available_variables. 31 | select { |name| name.to_s.start_with?(partial_name) }. 32 | map { |name| "#{prefix}#{name}" } 33 | end 34 | 35 | def prefix 36 | parse_input.first 37 | end 38 | 39 | def partial_name 40 | parse_input.last 41 | end 42 | 43 | def parse_input 44 | @parse_input ||= ( 45 | parts = input.rpartition(/\$\{?/) 46 | [parts[0...-1].join, parts.last] 47 | ) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/gitsh/tab_completion/visualization.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'gitsh/tab_completion/automaton' 3 | require 'gitsh/tab_completion/matchers/text_matcher' 4 | 5 | module Gitsh 6 | module TabCompletion 7 | class Visualization 8 | def initialize(automaton) 9 | @automaton = automaton 10 | end 11 | 12 | def to_dot 13 | %Q(digraph TabCompletion { 14 | ranksep=3; 15 | nodesep=3; 16 | #{visitor.node_labels.join("\n ")} 17 | #{visitor.transitions.join("\n ")} 18 | }) 19 | end 20 | 21 | def summary 22 | "#{visitor.node_labels.length} nodes\n"\ 23 | "#{visitor.transitions.length} edges" 24 | end 25 | 26 | private 27 | 28 | attr_reader :automaton 29 | 30 | def visitor 31 | @visitor ||= Visitor.new.tap do |visitor| 32 | automaton.accept_visitor(visitor) 33 | end 34 | end 35 | 36 | class Visitor 37 | attr_reader :node_labels, :transitions 38 | 39 | def initialize 40 | @node_labels = [] 41 | @transitions = [] 42 | end 43 | 44 | def visit_state(state) 45 | node_labels << '%s [ label=%s ];' % [ 46 | state_identifier(state), 47 | state.name.inspect, 48 | ] 49 | end 50 | 51 | def visit_transition(start_state, end_state, matcher) 52 | transitions << '%s -> %s [ label=%s ];' % [ 53 | state_identifier(start_state), 54 | state_identifier(end_state), 55 | transition_label(matcher).inspect, 56 | ] 57 | end 58 | 59 | def visit_free_transition(start_state, end_state) 60 | transitions << '%s -> %s;' % [ 61 | state_identifier(start_state), 62 | state_identifier(end_state), 63 | ] 64 | end 65 | 66 | private 67 | 68 | def state_identifier(state) 69 | state.object_id 70 | end 71 | 72 | def transition_label(matcher) 73 | case matcher 74 | when Gitsh::TabCompletion::Matchers::TextMatcher 75 | "\"#{matcher.word}\"" 76 | else 77 | "$#{matcher.name}" 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/gitsh/terminal.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require 'open3' 3 | 4 | module Gitsh 5 | class Terminal 6 | include Singleton 7 | 8 | class UnknownSizeError < StandardError; end 9 | 10 | def color_support? 11 | execute('tput colors').to_i > 0 12 | end 13 | 14 | def size 15 | size_from_stty || size_from_tput 16 | end 17 | 18 | private 19 | 20 | def size_from_stty 21 | size = execute('stty size') 22 | unless size.nil? 23 | size.split(/\s+/, 2).map(&:to_i) 24 | end 25 | end 26 | 27 | def size_from_tput 28 | [ 29 | lines_from_tput.to_i, 30 | cols_from_tput.to_i, 31 | ] 32 | end 33 | 34 | def lines_from_tput 35 | execute('env LINES="" tput lines') || 36 | execute('tput lines') || 37 | raise(UnknownSizeError, 'Cannot determine terminal size') 38 | end 39 | 40 | def cols_from_tput 41 | execute('env COLUMNS="" tput cols') || 42 | execute('tput cols') || 43 | raise(UnknownSizeError, 'Cannot determine terminal size') 44 | end 45 | 46 | def execute(command) 47 | output = IO.popen(command, err: '/dev/null') { |io| io.read } 48 | if $?.success? 49 | output.chomp 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/gitsh/version.rb.in: -------------------------------------------------------------------------------- 1 | module Gitsh 2 | VERSION = '@PACKAGE_VERSION@' 3 | end 4 | -------------------------------------------------------------------------------- /m4/ax_prog_ruby_version.m4: -------------------------------------------------------------------------------- 1 | # =========================================================================== 2 | # http://www.gnu.org/software/autoconf-archive/ax_prog_ruby_version.html 3 | # =========================================================================== 4 | # 5 | # SYNOPSIS 6 | # 7 | # AX_PROG_RUBY_VERSION([VERSION],[ACTION-IF-TRUE],[ACTION-IF-FALSE]) 8 | # 9 | # DESCRIPTION 10 | # 11 | # Makes sure that ruby supports the version indicated. If true the shell 12 | # commands in ACTION-IF-TRUE are executed. If not the shell commands in 13 | # ACTION-IF-FALSE are run. Note if $RUBY is not set (for example by 14 | # running AC_CHECK_PROG or AC_PATH_PROG) the macro will fail. 15 | # 16 | # Example: 17 | # 18 | # AC_PATH_PROG([RUBY],[ruby]) 19 | # AC_PROG_RUBY_VERSION([1.8.0],[ ... ],[ ... ]) 20 | # 21 | # This will check to make sure that the ruby you have supports at least 22 | # version 1.6.0. 23 | # 24 | # NOTE: This macro uses the $RUBY variable to perform the check. 25 | # AX_WITH_PROG([RUBY],[ruby],[VALUE-IF-NOT-FOUND],[PATH]) can be used to 26 | # set that variable prior to running this macro. The $RUBY_VERSION 27 | # variable will be valorized with the detected version. 28 | # 29 | # LICENSE 30 | # 31 | # Copyright (c) 2009 Francesco Salvestrini 32 | # 33 | # Copying and distribution of this file, with or without modification, are 34 | # permitted in any medium without royalty provided the copyright notice 35 | # and this notice are preserved. This file is offered as-is, without any 36 | # warranty. 37 | 38 | #serial 11 39 | 40 | AC_DEFUN([AX_PROG_RUBY_VERSION],[ 41 | AC_REQUIRE([AC_PROG_SED]) 42 | AC_REQUIRE([AC_PROG_GREP]) 43 | 44 | AS_IF([test -n "$RUBY"],[ 45 | ax_ruby_version="$1" 46 | 47 | AC_MSG_CHECKING([for ruby version]) 48 | changequote(<<,>>) 49 | ruby_version=`$RUBY --version 2>&1 | $GREP "^ruby " | $SED -e 's/^.* \([0-9]*\.[0-9]*\.[0-9]*\) .*/\1/'` 50 | changequote([,]) 51 | AC_MSG_RESULT($ruby_version) 52 | 53 | AC_SUBST([RUBY_VERSION],[$ruby_version]) 54 | 55 | AX_COMPARE_VERSION([$ax_ruby_version],[le],[$ruby_version],[ 56 | : 57 | $2 58 | ],[ 59 | : 60 | $3 61 | ]) 62 | ],[ 63 | AC_MSG_WARN([could not find the ruby interpreter]) 64 | $3 65 | ]) 66 | ]) 67 | -------------------------------------------------------------------------------- /spec/fixtures/fake_git: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cmd="" 4 | 5 | # Drop `-c ...` arguments which are used to pass gitsh environment 6 | # variables to git as configuration, and therefore do not form part 7 | # of the command the user issued. 8 | while [ "$1" != "" ]; do 9 | case $1 in 10 | -c) 11 | shift 12 | shift 13 | ;; 14 | *) 15 | cmd="$cmd $1" 16 | shift 17 | ;; 18 | esac 19 | done 20 | 21 | echo "Fake git:$cmd" 22 | -------------------------------------------------------------------------------- /spec/integration/arguments_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/cli' 3 | require 'gitsh/environment' 4 | 5 | describe '--version' do 6 | it 'outputs the version, and then exits' do 7 | output = StringIO.new 8 | error = StringIO.new 9 | env = Gitsh::Environment.new(output_stream: output, error_stream: error) 10 | 11 | runner = lambda do 12 | Gitsh::CLI.new(args: %w(--version), env: env).run 13 | end 14 | 15 | expect(runner).to raise_error SystemExit 16 | expect(error.string).to be_empty 17 | expect(output.string.chomp).to eq Gitsh::VERSION 18 | end 19 | end 20 | 21 | describe 'Unexpected arguments' do 22 | %w(--badger -x).each do |argument| 23 | context "with the argument #{argument.inspect}" do 24 | it 'outputs a usage message and exits' do 25 | output = StringIO.new 26 | error = StringIO.new 27 | env = Gitsh::Environment.new(output_stream: output, error_stream: error) 28 | 29 | runner = lambda do 30 | Gitsh::CLI.new(args: [argument], env: env).run 31 | end 32 | 33 | expect(runner).to raise_error SystemExit 34 | expect(output.string).to be_empty 35 | expect(error.string.chomp).to eq( 36 | 'usage: gitsh [--version] [-h | --help] [--git PATH] [script]' 37 | ) 38 | end 39 | end 40 | end 41 | end 42 | 43 | describe '--git' do 44 | it 'uses the requested git binary' do 45 | GitshRunner.interactive(args: ['--git', fake_git_path]) do |gitsh| 46 | gitsh.type('init') 47 | 48 | expect(gitsh).to output_no_errors 49 | expect(gitsh).to output(/^Fake git: init$/) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/integration/cd_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'The :cd command' do 4 | it 'changes the current working directory' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type 'init' 7 | 8 | Dir.mktmpdir do |path| 9 | gitsh.type ":cd #{path}" 10 | 11 | expect(gitsh).to output_no_errors 12 | expect(gitsh).to prompt_with "#{File.basename(path)} uninitialized!! " 13 | end 14 | end 15 | end 16 | 17 | it 'changes to the repository root directory when given no arguments' do 18 | GitshRunner.interactive do |gitsh| 19 | root_path = Dir.pwd 20 | gitsh.type 'init' 21 | Dir.mkdir 'subdir' 22 | Dir.chdir 'subdir' 23 | 24 | gitsh.type ':cd' 25 | 26 | expect(gitsh).to output_no_errors 27 | expect(gitsh).to prompt_with "#{File.basename(root_path)} master@ " 28 | end 29 | end 30 | 31 | it 'outputs helpful messages when given bad arguments' do 32 | GitshRunner.interactive do |gitsh| 33 | gitsh.type ':cd /not-a-real-path' 34 | 35 | expect(gitsh).to output_error(/gitsh: cd: No such directory/) 36 | 37 | gitsh.type ":cd #{__FILE__}" 38 | 39 | expect(gitsh).to output_error(/gitsh: cd: Not a directory/) 40 | end 41 | end 42 | 43 | it 'expands ~ in paths' do 44 | GitshRunner.interactive do |gitsh| 45 | gitsh.type ':cd ~' 46 | 47 | expect(gitsh).to output_no_errors 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/integration/coloring_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Color support' do 4 | include Color 5 | 6 | it 'is disabled for old terminals' do 7 | GitshRunner.interactive do |gitsh| 8 | expect(gitsh).to prompt_with "#{cwd_basename} uninitialized!! " 9 | end 10 | end 11 | 12 | it 'is enabled for color xterm' do 13 | GitshRunner.interactive(env: { 'TERM' => 'xterm-color' }) do |gitsh| 14 | expect(gitsh).to prompt_with( 15 | "#{cwd_basename} #{red_background}uninitialized!!#{clear} " 16 | ) 17 | end 18 | end 19 | 20 | it 'allows custom colors from git-config variables' do 21 | GitshRunner.interactive(env: { 'TERM' => 'xterm-color' }) do |gitsh| 22 | gitsh.type('config --global gitsh.color.uninitialized red') 23 | 24 | expect(gitsh).to prompt_with( 25 | "#{cwd_basename} #{red}uninitialized!!#{clear} " 26 | ) 27 | end 28 | end 29 | 30 | it 'allows custom colors from gitsh environment variables' do 31 | GitshRunner.interactive(env: { 'TERM' => 'xterm-color' }) do |gitsh| 32 | gitsh.type(':set gitsh.color.uninitialized blue') 33 | 34 | expect(gitsh).to prompt_with( 35 | "#{cwd_basename} #{blue}uninitialized!!#{clear} " 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/integration/command_arguments_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Command arguments' do 4 | it 'supports empty strings' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type ':echo Hello "" World' 7 | 8 | expect(gitsh).to output_no_errors 9 | expect(gitsh).to output "Hello World\n" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/integration/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Comments' do 4 | it 'supports commenting out an entire command with #' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type '#cd' 7 | 8 | expect(gitsh).to output_no_errors 9 | expect(gitsh).to output_nothing 10 | end 11 | end 12 | 13 | it 'supports commenting out part of a command with #' do 14 | GitshRunner.interactive do |gitsh| 15 | gitsh.type 'init' 16 | gitsh.type 'commit --allow-empty -m Message # Comment' 17 | 18 | expect(gitsh).to output_no_errors 19 | 20 | gitsh.type 'show HEAD' 21 | 22 | expect(gitsh).not_to output(/Comment/) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/integration/completion_errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/parse_error' 3 | 4 | describe 'Invalid completion file' do 5 | context 'with syntax errors' do 6 | it 'fails to start up in interactive mode' do 7 | with_a_temporary_home_directory do |home| 8 | config_path = "#{home}/.gitsh_completions" 9 | write_file(config_path, "??????\n******\n+++++++") 10 | 11 | expect { starting_gitsh }.to raise_exception( 12 | Gitsh::TabCompletion::DSL::ParseError, 13 | /Unexpected operator \(\?\) at line 1, column 1 in file #{config_path}/, 14 | ) 15 | end 16 | end 17 | end 18 | 19 | context 'with invalid variables' do 20 | it 'fails to start up in interactive mode' do 21 | with_a_temporary_home_directory do |home| 22 | config_path = "#{home}/.gitsh_completions" 23 | write_file(config_path, 'add $path $invalid_variable') 24 | 25 | expect { starting_gitsh }.to raise_exception( 26 | Gitsh::TabCompletion::DSL::ParseError, 27 | /Invalid variable \(\$invalid_variable\) at line 1, column 11 in file #{config_path}/, 28 | ) 29 | end 30 | end 31 | end 32 | 33 | def starting_gitsh 34 | GitshRunner.new.start_interactive 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/integration/correction_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Correcting input' do 4 | context 'when help.autocorrect is enabled' do 5 | it 'removes the git prefix from a command' do 6 | GitshRunner.interactive do |gitsh| 7 | gitsh.type ':set help.autocorrect 1' 8 | gitsh.type 'git init' 9 | 10 | expect(gitsh).to output_no_errors 11 | expect(gitsh).to prompt_with "#{cwd_basename} master@ " 12 | end 13 | end 14 | end 15 | 16 | context 'when help.autocorrect is disabled' do 17 | it 'errors when given a command with a git prefix' do 18 | GitshRunner.interactive do |gitsh| 19 | gitsh.type ':set help.autocorrect 0' 20 | gitsh.type 'git init' 21 | 22 | expect(gitsh).to output_error(/not a git command/) 23 | expect(gitsh).to prompt_with "#{cwd_basename} uninitialized!! " 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/create_repository_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/cli' 3 | 4 | describe 'Creating a repository' do 5 | it 'is possible through the gish CLI' do 6 | GitshRunner.interactive do |gitsh| 7 | expect(gitsh).to prompt_with "#{cwd_basename} uninitialized!! " 8 | 9 | gitsh.type('init') 10 | 11 | expect(gitsh).to output(/^Initialized empty Git repository/) 12 | expect(gitsh).to output_no_errors 13 | expect(gitsh).to prompt_with "#{cwd_basename} master@ " 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/integration/default_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Entering no command' do 4 | it 'runs `git status`' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type('init') 7 | gitsh.type('') 8 | 9 | expect(gitsh).to output(/nothing to commit/) 10 | end 11 | end 12 | 13 | it 'runs `git status` ignoring white space' do 14 | GitshRunner.interactive do |gitsh| 15 | gitsh.type('init') 16 | gitsh.type(' ') 17 | 18 | expect(gitsh).to output(/nothing to commit/) 19 | end 20 | end 21 | 22 | it 'can be overriden using a git-config variable' do 23 | GitshRunner.interactive do |gitsh| 24 | gitsh.type('init') 25 | gitsh.type('config --local gitsh.defaultCommand "show HEAD"') 26 | gitsh.type('commit --allow-empty -m First') 27 | gitsh.type('') 28 | 29 | expect(gitsh).to output(/First/) 30 | 31 | gitsh.type('commit --allow-empty -m Second') 32 | gitsh.type('') 33 | 34 | expect(gitsh).to output(/Second/) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/integration/default_git_path_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Default git path' do 4 | it 'can be overridden using a git-config variable' do 5 | GitshRunner.interactive( 6 | settings: { 'gitsh.gitCommand' => fake_git_path }, 7 | ) do |gitsh| 8 | gitsh.type('init') 9 | 10 | expect(gitsh).to output_no_errors 11 | expect(gitsh).to output(/^Fake git: init/) 12 | end 13 | end 14 | 15 | it 'overrides the configured default when specified with --git' do 16 | GitshRunner.interactive( 17 | settings: { 'gitsh.gitCommand' => '/usr/bin/env git' }, 18 | args: ['--git', fake_git_path] 19 | ) do |gitsh| 20 | gitsh.type('init') 21 | 22 | expect(gitsh).to output_no_errors 23 | expect(gitsh).to output(/^Fake git: init/) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/integration/error_handling_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Handling errors' do 4 | it 'does not explode when given an unknown internal command' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type(':foobar') 7 | 8 | expect(gitsh).to output_error(/gitsh: foobar: command not found/) 9 | end 10 | end 11 | 12 | it 'does not explode when given a badly formatted command' do 13 | GitshRunner.interactive do |gitsh| 14 | gitsh.type('add . && || commit') 15 | 16 | expect(gitsh).to output_error(/gitsh: parse error/) 17 | end 18 | end 19 | 20 | it 'does not explode when given a badly formatted script' do 21 | in_a_temporary_directory do 22 | write_file('bad.gitsh', ":echo 'foo") 23 | 24 | expect("#{gitsh_path} bad.gitsh"). 25 | to execute.with_exit_status(1). 26 | with_error_output_matching(/parse error/) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/integration/escaping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Escaping commands' do 4 | it 'does not pass arbitrary strings to a shell' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type('init ; echo Injection') 7 | 8 | expect(gitsh).not_to output(/Injection/) 9 | expect(gitsh).to output_error(/echo/) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/integration/gitshrc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'A .gitshrc file in the home directory' do 4 | context 'when it exists' do 5 | it 'is loaded when gitsh starts' do 6 | with_a_temporary_home_directory do 7 | write_file(gitshrc_path, ':set gitshrc_loaded "Config loaded"') 8 | GitshRunner.interactive do |gitsh| 9 | gitsh.type ':echo $gitshrc_loaded' 10 | 11 | expect(gitsh).to output(/Config loaded/) 12 | expect(gitsh).to output_no_errors 13 | end 14 | end 15 | end 16 | end 17 | 18 | context 'when it does not exist' do 19 | it 'does not cause an error' do 20 | GitshRunner.interactive do |gitsh| 21 | expect(File.exist?(gitshrc_path)).to be_falsey 22 | expect(gitsh).to output_no_errors 23 | end 24 | end 25 | end 26 | 27 | def gitshrc_path 28 | "#{ENV['HOME']}/.gitshrc" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/integration/greeting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Displaying a welcome message when gitsh starts' do 4 | it 'helps users understand what is going on' do 5 | GitshRunner.interactive do |gitsh| 6 | expect(gitsh).to output(/gitsh #{Gitsh::VERSION}\nType :exit to exit/) 7 | end 8 | end 9 | 10 | it 'can be disabled' do 11 | settings = { 'gitsh.noGreeting' => 'true' } 12 | GitshRunner.interactive(settings: settings) do |gitsh| 13 | expect(gitsh).to output_nothing 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/integration/help_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'The :help command' do 4 | it 'outputs a list of available built-in commands' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type ':help' 7 | 8 | expect(gitsh).to output_no_errors 9 | expect(gitsh).to output %r(Type :help \[command\] for more specific info) 10 | expect(gitsh).to output %r(You may use the following built-in commands:) 11 | expect(gitsh).to output %r(:exit) 12 | end 13 | end 14 | 15 | it 'outputs specific help for an individual built-in command' do 16 | GitshRunner.interactive do |gitsh| 17 | gitsh.type ':help set' 18 | 19 | expect(gitsh).to output_no_errors 20 | expect(gitsh).to output %r(usage: :set variable value) 21 | expect(gitsh).to output %r(Sets a variable in the gitsh environment to the given value) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/integration/inputrc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'A .inputrc file in the home directory' do 4 | RELOAD_INPUTRC = "\cx\cr".freeze 5 | 6 | it 'is used by gitsh' do 7 | with_a_temporary_home_directory do 8 | write_file(inputrc_path, <<-INPUTRC) 9 | $if gitsh 10 | "\\C-xx": ":echo this is a test" 11 | $endif 12 | INPUTRC 13 | 14 | GitshRunner.interactive do |gitsh| 15 | gitsh.type 'init' 16 | gitsh.type "#{RELOAD_INPUTRC}\cxx" 17 | 18 | expect(gitsh).to output_no_errors 19 | expect(gitsh).to output(/this is a test/) 20 | end 21 | end 22 | end 23 | 24 | def inputrc_path 25 | "#{ENV['HOME']}/.inputrc" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/multi_line_input_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Multi-line input' do 4 | it 'supports escaped line breaks within commands' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type(':echo Hello \\') 7 | 8 | expect(gitsh).to output_no_errors 9 | expect(gitsh).to prompt_with('> ') 10 | 11 | gitsh.type('world') 12 | 13 | expect(gitsh).to output_no_errors 14 | expect(gitsh).to output(/Hello world/) 15 | end 16 | end 17 | 18 | it 'supports line breaks after logical operators' do 19 | GitshRunner.interactive do |gitsh| 20 | gitsh.type(':echo Hello &&') 21 | 22 | expect(gitsh).to output_no_errors 23 | expect(gitsh).to prompt_with('> ') 24 | 25 | gitsh.type(':echo World') 26 | 27 | expect(gitsh).to output_no_errors 28 | expect(gitsh).to output(/Hello\nWorld/) 29 | end 30 | end 31 | 32 | it 'supports line breaks within strings' do 33 | GitshRunner.interactive do |gitsh| 34 | gitsh.type(':echo "Hello, world') 35 | 36 | expect(gitsh).to output_no_errors 37 | expect(gitsh).to prompt_with('> ') 38 | 39 | gitsh.type('') 40 | gitsh.type('Goodbye, world"') 41 | 42 | expect(gitsh).to output(/\AHello, world\n\nGoodbye, world\Z/) 43 | end 44 | end 45 | 46 | it 'supports line breaks within parentheses' do 47 | GitshRunner.interactive do |gitsh| 48 | gitsh.type('(:echo 1') 49 | 50 | expect(gitsh).to output_no_errors 51 | expect(gitsh).to prompt_with('> ') 52 | 53 | gitsh.type(':echo 2') 54 | gitsh.type(':echo 3)') 55 | 56 | expect(gitsh).to output_no_errors 57 | expect(gitsh).to output(/1\n2\n3/) 58 | end 59 | end 60 | 61 | it 'supports line breaks within subshells' do 62 | GitshRunner.interactive do |gitsh| 63 | gitsh.type(':echo $(') 64 | gitsh.type(' :set greeting Hello') 65 | gitsh.type(' :echo $greeting') 66 | gitsh.type(')') 67 | 68 | expect(gitsh).to output_no_errors 69 | expect(gitsh).to output(/Hello/) 70 | end 71 | end 72 | 73 | it 'supports comments in the middle of multi-line commands' do 74 | GitshRunner.interactive do |gitsh| 75 | gitsh.type('(:echo 1 # comment') 76 | 77 | expect(gitsh).to output_no_errors 78 | expect(gitsh).to prompt_with('> ') 79 | 80 | gitsh.type(':echo 2') 81 | gitsh.type('# another comment') 82 | gitsh.type(')') 83 | 84 | expect(gitsh).to output_no_errors 85 | expect(gitsh).to output(/1\n2/) 86 | end 87 | end 88 | 89 | it 'supports line breaks within strings in scripts' do 90 | in_a_temporary_directory do 91 | write_file('multiline.gitsh', ":echo 'foo\nbar'") 92 | 93 | expect("#{gitsh_path} multiline.gitsh"). 94 | to execute.successfully. 95 | with_output_matching(/foo\nbar/) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/integration/persistent_history_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Gitsh history' do 4 | it 'is persisted between sessions' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type('foobarbaz') 7 | expect(gitsh).to output_error(/'foobarbaz' is not a git command/) 8 | end 9 | 10 | GitshRunner.interactive do |gitsh| 11 | gitsh.type(GitshRunner::UP_ARROW * 2) 12 | expect(gitsh).to output_error(/'foobarbaz' is not a git command/) 13 | end 14 | end 15 | 16 | it 'uses the gitsh.historyFile setting' do 17 | with_a_temporary_home_directory do 18 | settings = { 'gitsh.historyFile' => '~/my_gitsh_history' } 19 | GitshRunner.interactive(settings: settings) do |gitsh| 20 | gitsh.type('foobarbaz') 21 | end 22 | 23 | expect(File.read("#{ENV['HOME']}/my_gitsh_history")).to match(/foobarbaz/) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/integration/prompt_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe 'The gitsh prompt' do 6 | it 'defaults to the directory basename and the branch name' do 7 | GitshRunner.interactive do |gitsh| 8 | gitsh.type('init') 9 | 10 | expect(gitsh).to prompt_with "#{cwd_basename} master@ " 11 | end 12 | end 13 | 14 | it 'defaults to abbreviated branch names' do 15 | GitshRunner.interactive do |gitsh| 16 | gitsh.type('init') 17 | gitsh.type("checkout -b best-branch-name-ever-forever") 18 | 19 | expect(gitsh).to prompt_with "#{cwd_basename} best-branch-nam…@ " 20 | end 21 | end 22 | 23 | it 'can be customised with a Git config variable' do 24 | GitshRunner.interactive do |gitsh| 25 | gitsh.type('init') 26 | gitsh.type('config gitsh.prompt "on %b %#"') 27 | 28 | expect(gitsh).to prompt_with 'on master @ ' 29 | end 30 | end 31 | 32 | it 'can be customised with a gitsh environment variable' do 33 | GitshRunner.interactive do |gitsh| 34 | gitsh.type('init') 35 | gitsh.type(':set gitsh.prompt "%d:%b%#"') 36 | 37 | expect(gitsh).to prompt_with "#{Dir.getwd}:master@ " 38 | end 39 | end 40 | 41 | it 'displays the repository status using prompt sigils' do 42 | GitshRunner.interactive do |gitsh| 43 | expect(gitsh).to prompt_with "#{cwd_basename} uninitialized!! " 44 | 45 | gitsh.type('init') 46 | 47 | expect(gitsh).to prompt_with "#{cwd_basename} master@ " 48 | 49 | write_file 'example.txt' 50 | gitsh.type('') 51 | 52 | expect(gitsh).to prompt_with "#{cwd_basename} master! " 53 | 54 | gitsh.type('add --intent-to-add example.txt') 55 | 56 | expect(gitsh).to prompt_with "#{cwd_basename} master& " 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/integration/running_scripts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Executing gitsh scripts' do 4 | context 'when the path to the script is passed as an argument' do 5 | it 'runs the script and exits' do 6 | in_a_temporary_directory do 7 | write_file('myscript.gitsh', "init\n\ncommit") 8 | 9 | expect("#{gitsh_path} --git #{fake_git_path} myscript.gitsh").to execute. 10 | successfully. 11 | with_output_matching(/^Fake git: init\nFake git: commit\n$/) 12 | end 13 | end 14 | 15 | context 'when the script file does not exist' do 16 | it 'exits with a useful error message' do 17 | expect("#{gitsh_path} --git #{fake_git_path} noscript.gitsh").to execute. 18 | with_exit_status(66). 19 | with_error_output_matching(/^gitsh: noscript\.gitsh: No such file or directory$/) 20 | end 21 | end 22 | end 23 | 24 | context 'when the script is piped to standard input' do 25 | it 'runs the script and exits' do 26 | in_a_temporary_directory do 27 | write_file('myscript.gitsh', "init\n\ncommit") 28 | 29 | expect("cat myscript.gitsh | #{gitsh_path} --git #{fake_git_path}"). 30 | to execute.successfully. 31 | with_output_matching(/^Fake git: init\nFake git: commit\n$/) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/shell_commands_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Executing a shell command' do 4 | it 'accepts a shell command prefixed with a !' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type '!echo Hello world' 7 | 8 | expect(gitsh).to output_no_errors 9 | expect(gitsh).to output 'Hello world' 10 | end 11 | end 12 | 13 | it 'accepts a shell command with no arguments' do 14 | GitshRunner.interactive do |gitsh| 15 | gitsh.type '!pwd' 16 | 17 | expect(gitsh).to output_no_errors 18 | expect(gitsh).to output(Dir.getwd) 19 | end 20 | end 21 | 22 | it 'accepts a relative shell command with no arguments' do 23 | GitshRunner.interactive do |gitsh| 24 | IO.write('./script', "#!/bin/sh\necho Hello world", 0, perm: 0700) 25 | 26 | gitsh.type '!./script' 27 | 28 | expect(gitsh).to output_no_errors 29 | expect(gitsh).to output 'Hello world' 30 | end 31 | end 32 | 33 | it 'uses a shell to expand arguments' do 34 | GitshRunner.interactive do |gitsh| 35 | write_file('myfile.txt', 'Hello world') 36 | 37 | gitsh.type '!ls *' 38 | 39 | expect(gitsh).to output_no_errors 40 | expect(gitsh).to output 'myfile.txt' 41 | end 42 | end 43 | 44 | it 'handles errors gracefully' do 45 | GitshRunner.interactive do |gitsh| 46 | gitsh.type '!notarealcommand' 47 | 48 | expect(gitsh).to output_error(/not found/) 49 | expect(gitsh).to output_nothing 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/integration/source_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'The :source command' do 4 | context 'a source file is given' do 5 | it 'executes the commands in the sourced file' do 6 | GitshRunner.interactive do |gitsh| 7 | write_file('.gitshrc', ':set source_worked "Yes it did!"') 8 | 9 | gitsh.type ':source .gitshrc' 10 | gitsh.type ':echo $source_worked' 11 | 12 | expect(gitsh).to output_no_errors 13 | expect(gitsh).to output(/Yes it did!/) 14 | end 15 | end 16 | 17 | it 'expands ~ in paths' do 18 | GitshRunner.interactive do |gitsh| 19 | write_file("#{ENV['HOME']}/.gitshrc", ':set source_worked "True"') 20 | 21 | gitsh.type ':source ~/.gitshrc' 22 | gitsh.type ':echo $source_worked' 23 | 24 | expect(gitsh).to output_no_errors 25 | expect(gitsh).to output(/True/) 26 | end 27 | end 28 | end 29 | 30 | context 'no source file is given' do 31 | it 'prints an error message' do 32 | GitshRunner.interactive do |gitsh| 33 | gitsh.type ':source' 34 | 35 | expect(gitsh).to output_error(/usage/) 36 | expect(gitsh).to output_nothing 37 | end 38 | end 39 | end 40 | 41 | context 'a missing source file is given' do 42 | it 'prints an error message' do 43 | GitshRunner.interactive do |gitsh| 44 | gitsh.type ':source not/a/real/file' 45 | 46 | expect(gitsh).to output_error(/No such file/) 47 | expect(gitsh).to output_nothing 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/integration/subshell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Subshell' do 4 | it 'supports subshells using $(...)' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type 'init' 7 | gitsh.type ':echo prefix $(status) suffix' 8 | 9 | expect(gitsh).to output_no_errors 10 | expect(gitsh).to output(/prefix.*nothing to commit.*suffix/) 11 | end 12 | end 13 | 14 | it 'does not modify the parent environment' do 15 | GitshRunner.interactive do |gitsh| 16 | gitsh.type ':set x "x in parent shell"' 17 | gitsh.type ':echo $(:set x "x in subshell")' 18 | gitsh.type ':echo $x' 19 | 20 | expect(gitsh).to output_no_errors 21 | expect(gitsh).to output(/x in parent shell/) 22 | end 23 | end 24 | 25 | it 'supports nested subshells' do 26 | GitshRunner.interactive do |gitsh| 27 | gitsh.type 'init' 28 | gitsh.type ':echo $(:echo $(status))' 29 | 30 | expect(gitsh).to output_no_errors 31 | expect(gitsh).to output(/nothing to commit/) 32 | end 33 | end 34 | 35 | it 'supports adjacent subshells' do 36 | GitshRunner.interactive do |gitsh| 37 | gitsh.type ':echo $(:echo foo)$(:echo bar)' 38 | 39 | expect(gitsh).to output_no_errors 40 | expect(gitsh).to output(/\bfoobar\b/) 41 | end 42 | end 43 | 44 | it 'supports subshells in larger argument lists' do 45 | GitshRunner.interactive do |gitsh| 46 | gitsh.type ':echo 1 $(:echo 2) 3' 47 | 48 | expect(gitsh).to output_no_errors 49 | expect(gitsh).to output(/\b1 2 3\b/) 50 | end 51 | end 52 | 53 | it 'is not confused by quoted parens in a subshell' do 54 | GitshRunner.interactive do |gitsh| 55 | gitsh.type ':echo $(:echo ")))")' 56 | 57 | expect(gitsh).to output_no_errors 58 | expect(gitsh).to output(/\)\)\)/) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/integration/variables_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Gitsh variables' do 4 | it 'can be set with the :set command and read with a dollar prefix' do 5 | GitshRunner.interactive do |gitsh| 6 | gitsh.type('init') 7 | gitsh.type(':set author "John Doe "') 8 | 9 | expect(gitsh).to output_no_errors 10 | 11 | gitsh.type(':set message "An initial commit"') 12 | 13 | expect(gitsh).to output_no_errors 14 | 15 | gitsh.type('commit --allow-empty --author $author -m $message') 16 | 17 | expect(gitsh).to output_no_errors 18 | 19 | gitsh.type('log --format="%ae - %s"') 20 | 21 | expect(gitsh).to output_no_errors 22 | expect(gitsh).to output(/^john@example\.com - An initial commit$/) 23 | end 24 | end 25 | 26 | it 'temporarily adds variables with a dot to git config' do 27 | GitshRunner.interactive do |gitsh| 28 | gitsh.type(':set test.example "This is a test"') 29 | 30 | expect(gitsh).to output_no_errors 31 | 32 | gitsh.type('config --get test.example') 33 | 34 | expect(gitsh).to output_no_errors 35 | expect(gitsh).to output(/This is a test/) 36 | end 37 | end 38 | 39 | it 'exposes config variables when read with a dot prefix' do 40 | GitshRunner.interactive do |gitsh| 41 | gitsh.type('init') 42 | gitsh.type('config test.example "A configuration variable"') 43 | gitsh.type('commit --allow-empty -m "test.example: $test.example"') 44 | 45 | expect(gitsh).to output_no_errors 46 | 47 | gitsh.type('log --format="%s" -n 1') 48 | 49 | expect(gitsh).to output_no_errors 50 | expect(gitsh).to output(/test\.example: A configuration variable/) 51 | end 52 | end 53 | 54 | it 'does not explode when :set is used incorrectly' do 55 | GitshRunner.interactive do |gitsh| 56 | gitsh.type(':set') 57 | 58 | expect(gitsh).to output_error(/usage: :set variable value/) 59 | end 60 | end 61 | 62 | it 'allows echoing of set variables' do 63 | GitshRunner.interactive do |gitsh| 64 | gitsh.type(':set greeting hello') 65 | gitsh.type(':echo $greeting') 66 | 67 | expect(gitsh).to output_no_errors 68 | expect(gitsh).to output(/hello/) 69 | end 70 | end 71 | 72 | it 'allows variables to be used as commands' do 73 | GitshRunner.interactive do |gitsh| 74 | gitsh.type(':set command :set') 75 | gitsh.type('$command greeting hello') 76 | 77 | expect(gitsh).to output_no_errors 78 | 79 | gitsh.type(':echo $greeting') 80 | 81 | expect(gitsh).to output_no_errors 82 | expect(gitsh).to output(/hello/) 83 | end 84 | end 85 | 86 | it 'errors when told to read an unset variable' do 87 | GitshRunner.interactive do |gitsh| 88 | gitsh.type(':echo "hello $unset world"') 89 | 90 | expect(gitsh).to output_nothing 91 | expect(gitsh).to output_error(/unset/) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | 3 | $LOAD_PATH.unshift(File.expand_path('../../ext', __FILE__)) 4 | 5 | Dir[File.expand_path('../support/**/*.rb', __FILE__)].each do |path| 6 | require path 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.order = 'random' 11 | end 12 | 13 | Pry.config.tap do |config| 14 | config.output = STDOUT 15 | config.input = STDIN 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/colors.rb: -------------------------------------------------------------------------------- 1 | require 'gitsh/colors' 2 | 3 | module Color 4 | def self.included(other) 5 | other.let(:clear) { Gitsh::Colors::CLEAR } 6 | other.let(:red_background) { Gitsh::Colors::RED_BG } 7 | other.let(:red) { Gitsh::Colors::RED_FG } 8 | other.let(:yellow) { Gitsh::Colors::YELLOW_FG } 9 | other.let(:cyan) { Gitsh::Colors::CYAN_FG } 10 | other.let(:blue) { Gitsh::Colors::BLUE_FG } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/delegate_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :delegate do |original_method| 2 | chain :to do |target_object, target_method| 3 | @target_object = target_object 4 | @target_method = target_method 5 | end 6 | 7 | match do |original_object| 8 | result = double 9 | allow(@target_object).to receive(@target_method). 10 | and_return(result) 11 | 12 | expect(original_object.send(original_method)).to eq result 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/execute_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :execute do 2 | chain :successfully do 3 | @exit_status_matcher = eq(0) 4 | @error_matcher = eq('') 5 | end 6 | 7 | chain :with_exit_status do |exit_status| 8 | @exit_status_matcher = eq(exit_status) 9 | end 10 | 11 | chain :with_output_matching do |output_pattern| 12 | @output_matcher = match_regex(output_pattern) 13 | end 14 | 15 | chain :with_error_output_matching do |output_pattern| 16 | @error_matcher = match_regex(output_pattern) 17 | end 18 | 19 | match do 20 | output, error, exit_status = Open3.capture3(actual) 21 | 22 | @output_matcher ||= be_empty 23 | 24 | [ 25 | @exit_status_matcher.matches?(exit_status.exitstatus), 26 | @output_matcher.matches?(output), 27 | @error_matcher.matches?(error), 28 | ].all? 29 | end 30 | 31 | failure_message do 32 | [ 33 | @exit_status_matcher.failure_message, 34 | @output_matcher.failure_message, 35 | @error_matcher.failure_message, 36 | ].join("\n") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/fake_line_editor.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'gitsh/line_editor' 3 | require 'gitsh/module_delegator' 4 | 5 | class FakeLineEditor < ModuleDelegator 6 | def initialize 7 | @prompt_queue = Queue.new 8 | @input_read, @input_write = IO.pipe 9 | super(Gitsh::LineEditor) 10 | end 11 | 12 | def readline(prompt, add_to_history) 13 | module_delegator_target.input = input_read 14 | module_delegator_target.output = output_file 15 | prompt_queue.clear 16 | prompt_queue << prompt 17 | module_delegator_target.readline(prompt, add_to_history) 18 | end 19 | 20 | def type(string) 21 | input_write << "#{string}\n" 22 | end 23 | 24 | def send_eof 25 | input_write.close 26 | end 27 | 28 | def prompt 29 | prompt_queue.pop 30 | end 31 | 32 | private 33 | 34 | attr_reader :prompt_queue, :input_read, :input_write 35 | 36 | def output_file 37 | if ENV['DEBUG'] 38 | $stdout 39 | else 40 | File.open(Tempfile.new('line_editor_out').path, 'w') 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/file_system.rb: -------------------------------------------------------------------------------- 1 | module FileSystemHelper 2 | DEFAULT_READLINE_CONFIG = 'set bell-style none' 3 | DEFAULT_GIT_CONFIG = "[user]\n name = Test\n email = test@example.com" 4 | 5 | def write_file(name, contents="Some content") 6 | File.open(name, 'w') { |f| f << "#{contents}\n" } 7 | end 8 | 9 | def make_directory(name) 10 | Dir.mkdir(name) 11 | end 12 | 13 | def temp_file(name, contents) 14 | Tempfile.new(name).tap do |f| 15 | f.write("#{contents}\n") 16 | f.flush 17 | end 18 | end 19 | 20 | def in_a_temporary_directory(&block) 21 | Dir.mktmpdir do |path| 22 | chdir_and_allow_nesting(path, &block) 23 | end 24 | end 25 | 26 | def with_a_temporary_home_directory(&block) 27 | if ENV['TEMP_HOME'] 28 | block.call(ENV['HOME']) 29 | else 30 | switch_home_directory(&block) 31 | end 32 | end 33 | 34 | def chdir_and_allow_nesting(path) 35 | original_path = Dir.getwd 36 | Dir.chdir(path) 37 | 38 | begin 39 | yield 40 | ensure 41 | Dir.chdir(original_path) 42 | end 43 | end 44 | 45 | private 46 | 47 | def switch_home_directory(&block) 48 | ENV['TEMP_HOME'] = 'TRUE' 49 | original_home = ENV['HOME'] 50 | 51 | Dir.mktmpdir do |path| 52 | ENV['HOME'] = path 53 | write_file("#{path}/.inputrc", DEFAULT_READLINE_CONFIG) 54 | write_file("#{path}/.gitconfig", DEFAULT_GIT_CONFIG) 55 | block.call(path) 56 | end 57 | ensure 58 | ENV['HOME'] = original_home 59 | ENV.delete('TEMP_HOME') 60 | end 61 | end 62 | 63 | RSpec.configure do |config| 64 | config.include FileSystemHelper 65 | end 66 | -------------------------------------------------------------------------------- /spec/support/fixtures.rb: -------------------------------------------------------------------------------- 1 | module FixturesHelper 2 | def fake_git_path 3 | File.expand_path('../../../spec/fixtures/fake_git', __FILE__) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include FixturesHelper 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/internal_command_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'an internal command' do 2 | it 'has a .help_message method' do 3 | expect { 4 | described_class.help_message 5 | }.to_not raise_exception 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/produce_tokens_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define(:produce_tokens) do |expected| 2 | match do |actual| 3 | @expected = expected.join("\n") 4 | @actual = described_class.lex(actual).map(&:to_s).join("\n") 5 | values_match? @expected, @actual 6 | end 7 | 8 | diffable 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/scripts.rb: -------------------------------------------------------------------------------- 1 | module Scripts 2 | def gitsh_path 3 | File.expand_path('../../../bin/gitsh', __FILE__) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include Scripts 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/signalling_line_editor.rb: -------------------------------------------------------------------------------- 1 | class SignallingLineEditor 2 | def initialize(signal) 3 | @signal = signal 4 | end 5 | 6 | def readline(*args, &block) 7 | Process.kill(signal, Process.pid) 8 | nil 9 | end 10 | 11 | def method_missing(name, *args, &block) 12 | end 13 | 14 | private 15 | 16 | attr_reader :signal 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/stubbed_method_result.rb: -------------------------------------------------------------------------------- 1 | class StubbedMethodResult 2 | def initialize 3 | @results = [] 4 | end 5 | 6 | def raises(error, message = nil) 7 | @results << proc { raise error.new(message) } 8 | 9 | self 10 | end 11 | 12 | def returns(value) 13 | @results << proc { value } 14 | 15 | self 16 | end 17 | 18 | def next_result 19 | @results.shift.call 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/tab_completion.rb: -------------------------------------------------------------------------------- 1 | module TabCompletionHelpers 2 | def stub_text_matcher(text) 3 | klass = Gitsh::TabCompletion::Matchers::TextMatcher 4 | matcher = instance_double(klass) 5 | allow(klass).to receive(:new).with(text).and_return(matcher) 6 | matcher 7 | end 8 | end 9 | 10 | RSpec.configure do |config| 11 | config.include TabCompletionHelpers 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/tokens.rb: -------------------------------------------------------------------------------- 1 | require 'rltk' 2 | 3 | module Tokens 4 | def tokens(*tokens) 5 | tokens.map.with_index do |token, i| 6 | type, value = token 7 | pos = RLTK::StreamPosition.new(i, 1, i, 10, nil) 8 | RLTK::Token.new(type, value, pos) 9 | end 10 | end 11 | end 12 | 13 | RSpec.configure do |config| 14 | config.include Tokens 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/working_directory.rb: -------------------------------------------------------------------------------- 1 | module WorkingDirectory 2 | def cwd_basename 3 | File.basename(Dir.getwd) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include WorkingDirectory 9 | end 10 | -------------------------------------------------------------------------------- /spec/units/argument_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/argument_list' 3 | 4 | describe Gitsh::ArgumentList do 5 | describe '#length' do 6 | it 'returns the number of arguments' do 7 | argument_list = Gitsh::ArgumentList.new(['hello', 'goodbye']) 8 | 9 | expect(argument_list.length).to eq 2 10 | end 11 | end 12 | 13 | describe '#values' do 14 | it 'returns the values of the arguments' do 15 | env = double('env') 16 | hello_arg = spy('hello_arg', value: 'hello') 17 | goodbye_arg = spy('goodbye_arg', value: 'goodbye') 18 | argument_list = Gitsh::ArgumentList.new([hello_arg, goodbye_arg]) 19 | 20 | expect(argument_list.values(env)).to eq ['hello', 'goodbye'] 21 | expect(hello_arg).to have_received(:value).with(env) 22 | expect(goodbye_arg).to have_received(:value).with(env) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/units/arguments/composite_argument_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/arguments/composite_argument' 3 | 4 | describe Gitsh::Arguments::CompositeArgument do 5 | describe '#value' do 6 | it 'returns the concatenated values of the arguments passed to the initializer' do 7 | env = double('env') 8 | first_argument = double('first_argument', value: 'Hello') 9 | second_argument = double('second_argument', value: 'World') 10 | argument = described_class.new([first_argument, second_argument]) 11 | 12 | expect(argument.value(env)).to eq 'HelloWorld' 13 | expect(first_argument).to have_received(:value).with(env) 14 | expect(second_argument).to have_received(:value).with(env) 15 | end 16 | end 17 | 18 | describe '#==' do 19 | it 'returns true when the parts are equal' do 20 | abc1 = described_class.new(['A', 'B', 'C']) 21 | abc2 = described_class.new(['A', 'B', 'C']) 22 | 23 | expect(abc1).to eq abc2 24 | end 25 | 26 | it 'returns false when the parts are not equal' do 27 | a = described_class.new(['A']) 28 | b = described_class.new(['B']) 29 | 30 | expect(a).not_to eq b 31 | end 32 | 33 | it 'returns false when the other object has a different class' do 34 | arg_a = described_class.new(['A']) 35 | double_a = double(parts: ['A']) 36 | 37 | expect(arg_a).not_to eq double_a 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/units/arguments/string_argument_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/arguments/string_argument' 3 | 4 | describe Gitsh::Arguments::StringArgument do 5 | describe '#value' do 6 | it 'returns the string passed to the initializer' do 7 | arg = described_class.new('Hello world') 8 | env = double('env') 9 | 10 | expect(arg.value(env)).to eq 'Hello world' 11 | end 12 | end 13 | 14 | describe '#==' do 15 | it 'returns true when the values of the arguments are equal' do 16 | a1 = described_class.new('A') 17 | a2 = described_class.new('A') 18 | 19 | expect(a1).to eq a2 20 | end 21 | 22 | it 'returns false when the values of the arguments are not equal' do 23 | a = described_class.new('A') 24 | b = described_class.new('B') 25 | 26 | expect(a).not_to eq b 27 | end 28 | 29 | it 'returns false when the other object has a different class' do 30 | arg_a = described_class.new('A') 31 | double_a = double(raw_value: 'A') 32 | 33 | expect(arg_a).not_to eq double_a 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/units/arguments/subshell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/arguments/subshell' 3 | 4 | describe Gitsh::Arguments::Subshell do 5 | describe '#value' do 6 | it 'returns the result of executing the subshell' do 7 | capturing_environment = stub_capturing_environment('expected output') 8 | env = double('env') 9 | wrapped_command = double(:command, execute: nil) 10 | subshell = described_class.new(wrapped_command) 11 | 12 | output = subshell.value(env) 13 | 14 | expect(output).to eq 'expected output' 15 | expect(wrapped_command).to have_received(:execute).with(capturing_environment) 16 | end 17 | end 18 | 19 | describe '#==' do 20 | it 'returns true when the commands are equal' do 21 | a1 = described_class.new('A') 22 | a2 = described_class.new('A') 23 | 24 | expect(a1).to eq a2 25 | end 26 | 27 | it 'returns false when the commands are not equal' do 28 | a = described_class.new('A') 29 | b = described_class.new('B') 30 | 31 | expect(a).not_to eq b 32 | end 33 | 34 | it 'returns false when the other object has a different class' do 35 | arg_a = described_class.new('A') 36 | double_a = double(command: 'A') 37 | 38 | expect(arg_a).not_to eq double_a 39 | end 40 | end 41 | 42 | def stub_capturing_environment(output) 43 | env = double(:capturing_environment, captured_output: output) 44 | allow(Gitsh::CapturingEnvironment).to receive(:new).and_return(env) 45 | env 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/units/arguments/variable_argument_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/arguments/variable_argument' 3 | 4 | describe Gitsh::Arguments::VariableArgument do 5 | describe '#value' do 6 | it 'returns the value of the variable passed to the initializer' do 7 | env = { 'author' => 'George' } 8 | argument = described_class.new('author') 9 | 10 | expect(argument.value(env)).to eq 'George' 11 | end 12 | end 13 | 14 | describe '#==' do 15 | it 'returns true when the variable names are equal' do 16 | a1 = described_class.new('A') 17 | a2 = described_class.new('A') 18 | 19 | expect(a1).to eq a2 20 | end 21 | 22 | it 'returns false when the variable names are not equal' do 23 | a = described_class.new('A') 24 | b = described_class.new('B') 25 | 26 | expect(a).not_to eq b 27 | end 28 | 29 | it 'returns false when the other object has a different class' do 30 | arg_a = described_class.new('A') 31 | double_a = double(variable_name: 'A') 32 | 33 | expect(arg_a).not_to eq double_a 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/units/capturing_environment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/capturing_environment' 3 | 4 | describe Gitsh::CapturingEnvironment do 5 | describe '#captured_output' do 6 | it 'returns any output written to the output stream directly' do 7 | env = double('env') 8 | capturing_env = described_class.new(env) 9 | capturing_env.output_stream.puts 'Hello, world' 10 | capturing_env.output_stream.puts 'Goodbye' 11 | 12 | expect(capturing_env.captured_output).to eq "Hello, world\nGoodbye\n" 13 | end 14 | 15 | it 'returns any output written to the output stream via #puts' do 16 | env = double('env') 17 | capturing_env = described_class.new(env) 18 | capturing_env.puts 'Hello, world' 19 | capturing_env.puts 'Goodbye' 20 | 21 | expect(capturing_env.captured_output).to eq "Hello, world\nGoodbye\n" 22 | end 23 | 24 | it 'returns any output written to the output stream via #print' do 25 | env = double('env') 26 | capturing_env = described_class.new(env) 27 | capturing_env.print 'Hello, world. ' 28 | capturing_env.print 'Goodbye.' 29 | 30 | expect(capturing_env.captured_output).to eq 'Hello, world. Goodbye.' 31 | end 32 | end 33 | 34 | describe 'delegations' do 35 | it 'delegates unknown methods to the wrapped environment' do 36 | return_value = double('return_value') 37 | env = double('env', foo: return_value) 38 | capturing_env = described_class.new(env) 39 | 40 | expect(capturing_env).to respond_to(:foo) 41 | expect(capturing_env.foo).to eq return_value 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/units/commands/internal/chdir_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Chdir do 5 | it_behaves_like "an internal command" 6 | 7 | describe '#execute' do 8 | it 'returns true for correct directories' do 9 | env = double('Environment', puts_error: true) 10 | command = described_class.new('cd', ['./']) 11 | 12 | expect(command.execute(env)).to be_truthy 13 | end 14 | 15 | it 'returns true for no argument' do 16 | env = double('Environment', puts_error: true) 17 | allow(env).to receive(:fetch).with(:_root).and_return(Dir.pwd) 18 | command = described_class.new('cd', []) 19 | 20 | expect(command.execute(env)).to be_truthy 21 | end 22 | 23 | it 'returns false with invalid arguments' do 24 | env = double('Environment', puts_error: true) 25 | command = described_class.new('cd', ['foo']) 26 | 27 | expect(command.execute(env)).to be_falsey 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/units/commands/internal/echo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Echo do 5 | it_behaves_like "an internal command" 6 | 7 | describe '#execute' do 8 | it 'prints all arguments to the environment joined with a space' do 9 | env = double('env', puts: nil) 10 | command = described_class.new('echo', ['foo', 'bar']) 11 | 12 | expect(command.execute(env)).to be_truthy 13 | expect(env).to have_received(:puts).with('foo bar') 14 | end 15 | 16 | it 'prints a newline when no arguments are passed' do 17 | env = double('env', puts: nil) 18 | command = described_class.new('echo', []) 19 | 20 | expect(command.execute(env)).to be_truthy 21 | expect(env).to have_received(:puts).with('') 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/units/commands/internal/exit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Exit do 5 | it_behaves_like "an internal command" 6 | 7 | describe '#execute' do 8 | it 'exits the program' do 9 | command = described_class.new('exit', []) 10 | expect { command.execute(double('env')) }.to raise_exception(SystemExit) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/units/commands/internal/help_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Help do 5 | it_behaves_like "an internal command" 6 | 7 | describe "#execute" do 8 | context "with no arguments" do 9 | it "prints out some stuff" do 10 | env = spy('env', puts: nil) 11 | command = described_class.new('help', []) 12 | 13 | expect(command.execute(env)).to be_truthy 14 | expect(env).to have_received(:puts).at_least(1).times 15 | end 16 | end 17 | 18 | context "with an argument that matches an existing command" do 19 | it "prints out command-specific information" do 20 | env = spy('env', puts: nil) 21 | command = described_class.new('help', ['set']) 22 | set_command = double('Set', help_message: 'Sets variables') 23 | allow(Gitsh::Commands::InternalCommand).to receive(:command_class). 24 | with('set'). 25 | and_return(set_command) 26 | 27 | expect(command.execute(env)).to be_truthy 28 | expect(env).to have_received(:puts).with('Sets variables') 29 | end 30 | end 31 | 32 | context 'with a colon-prefixed argument' do 33 | it 'strips the colon' do 34 | env = spy('env', puts: nil) 35 | command = described_class.new('help', [':set']) 36 | set_command = double('Set', help_message: 'Sets variables') 37 | allow(Gitsh::Commands::InternalCommand).to receive(:command_class). 38 | with('set'). 39 | and_return(set_command) 40 | 41 | expect(command.execute(env)).to be_truthy 42 | expect(env).to have_received(:puts).with('Sets variables') 43 | end 44 | end 45 | 46 | context "with arguments that don't match an existing command" do 47 | it "prints out some stuff" do 48 | env = spy('env', puts: nil) 49 | command = described_class.new( 50 | 'help', 51 | ["we don't do this here"], 52 | ) 53 | 54 | expect(command.execute(env)).to be_truthy 55 | expect(env).to have_received(:puts).at_least(1).times 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/units/commands/internal/set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Set do 5 | it_behaves_like "an internal command" 6 | 7 | describe '#execute' do 8 | it 'sets a variable on the environment' do 9 | env = spy('env') 10 | command = described_class.new('set', ['foo', 'bar']) 11 | 12 | command.execute(env) 13 | 14 | expect(env).to have_received(:[]=).with('foo', 'bar') 15 | end 16 | 17 | it 'returns true with correct arguments' do 18 | env = double('Environment', :[]= => true, puts_error: true) 19 | command = described_class.new('set', ['foo', 'bar']) 20 | 21 | expect(command.execute(env)).to be_truthy 22 | end 23 | 24 | it 'returns false with invalid arguments' do 25 | env = double('Environment', :[]= => true, puts_error: true) 26 | command = described_class.new('set', ['foo']) 27 | 28 | expect(command.execute(env)).to be_falsey 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/units/commands/internal/source_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Source do 5 | it_behaves_like "an internal command" 6 | 7 | describe "#execute" do 8 | context "with a valid file" do 9 | it "calls the FileRunner and returns true" do 10 | env = double('env') 11 | allow(Gitsh::FileRunner).to receive(:run) 12 | command = described_class.new('source', ['/path']) 13 | 14 | result = command.execute(env) 15 | 16 | expect(Gitsh::FileRunner).to have_received(:run). 17 | with(env: env, path: '/path') 18 | expect(result).to eq true 19 | end 20 | end 21 | 22 | context "with no file argument" do 23 | it "prints a usage message and returns false" do 24 | env = spy('env', puts_error: nil) 25 | command = described_class.new('source', []) 26 | 27 | result = command.execute(env) 28 | 29 | expect(env).to have_received(:puts_error).with('usage: :source path') 30 | expect(result).to eq false 31 | end 32 | end 33 | 34 | context 'with a file that fails to parse' do 35 | it 'prints an error message and returns false' do 36 | env = spy('env', puts_error: nil) 37 | command = described_class.new('source', ['/bad_script']) 38 | allow(Gitsh::FileRunner). 39 | to receive(:run).and_raise(Gitsh::ParseError, 'Oh no!') 40 | 41 | result = command.execute(env) 42 | 43 | expect(env).to have_received(:puts_error).with('gitsh: Oh no!') 44 | expect(result).to eq false 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/units/commands/internal/unknown_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand::Unknown do 5 | it_behaves_like "an internal command" 6 | 7 | describe '#execute' do 8 | it 'outputs an error message' do 9 | env = spy('env', puts_error: nil) 10 | command = described_class.new('notacommand', []) 11 | 12 | command.execute(env) 13 | 14 | expect(env).to have_received(:puts_error).with( 15 | 'gitsh: notacommand: command not found' 16 | ) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/units/commands/internal_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/internal_command' 3 | 4 | describe Gitsh::Commands::InternalCommand do 5 | describe '.new' do 6 | it 'returns a Set command when given the command "set"' do 7 | command = described_class.new('set', %w(foo bar)) 8 | expect(command).to be_a described_class::Set 9 | end 10 | 11 | it 'returns an Exit command when given the command "exit"' do 12 | command = described_class.new('exit', []) 13 | expect(command).to be_a described_class::Exit 14 | end 15 | 16 | it 'returns an Exit command when given the command "q"' do 17 | command = described_class.new('q', []) 18 | expect(command).to be_a described_class::Exit 19 | end 20 | 21 | it 'returns a Chdir command when given the command "cd"' do 22 | command = described_class.new('cd', '/some/path') 23 | expect(command).to be_a described_class::Chdir 24 | end 25 | 26 | it 'returns a Help command when given the command "help"' do 27 | command = described_class.new('help', []) 28 | expect(command).to be_a described_class::Help 29 | end 30 | 31 | it 'returns a Source command when given the command "source"' do 32 | command = described_class.new('source', ['/some/path']) 33 | expect(command).to be_a described_class::Source 34 | end 35 | 36 | it 'returns an Unknown command when given anything else' do 37 | command = described_class.new('notacommand', %w(foo bar)) 38 | expect(command).to be_a described_class::Unknown 39 | end 40 | end 41 | 42 | describe '.commands' do 43 | it 'returns a list of recognised commands formatted for autocomplete' do 44 | expect(described_class.commands).to include ':set', ':exit' 45 | end 46 | end 47 | 48 | describe '.command_class' do 49 | it 'returns a class object corresponding to the command' do 50 | expect(described_class.command_class('exit')).to eq described_class::Exit 51 | end 52 | 53 | it 'returns Unknown for an unknown command' do 54 | expect(described_class.command_class('banana')).to eq described_class::Unknown 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/units/commands/shell_command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/commands/shell_command' 3 | 4 | describe Gitsh::Commands::ShellCommand do 5 | describe '#execute' do 6 | it 'delegates to the Gitsh::ShellCommandRunner' do 7 | env = double(:env) 8 | expected_result = double(:result) 9 | mock_runner = double(:shell_command_runner, run: expected_result) 10 | 11 | command = described_class.new( 12 | 'echo', 13 | ['Hello', 'world'], 14 | shell_command_runner: mock_runner, 15 | ) 16 | result = command.execute(env) 17 | 18 | expect(mock_runner).to have_received(:run).with( 19 | ['/bin/sh', '-c', 'echo Hello world'], 20 | env, 21 | ) 22 | expect(result).to eq expected_result 23 | end 24 | 25 | it 'escapes special characters in arguments' do 26 | env = double(:env) 27 | mock_runner = double(:shell_command_runner, run: double(:result)) 28 | args = ['with space', '^$'] 29 | escaped_args = ['with\\ space', '\\^\\$'] 30 | 31 | described_class. 32 | new('echo', args, shell_command_runner: mock_runner). 33 | execute(env) 34 | 35 | expect(mock_runner).to have_received(:run).with( 36 | ['/bin/sh', '-c', "echo #{escaped_args.join(' ')}"], 37 | env, 38 | ) 39 | end 40 | 41 | it 'does not escape globbing patterns in arguments' do 42 | env = double(:env) 43 | mock_runner = double(:shell_command_runner, run: double(:result)) 44 | args = ['*', '[a-z]', '[!a-z]', '?', '\\*'] 45 | 46 | described_class. 47 | new('echo', args, shell_command_runner: mock_runner). 48 | execute(env) 49 | 50 | expect(mock_runner).to have_received(:run).with( 51 | ['/bin/sh', '-c', "echo #{args.join(' ')}"], 52 | env, 53 | ) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/units/file_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/file_runner' 3 | 4 | describe Gitsh::FileRunner do 5 | describe '#run' do 6 | it 'executes the given script' do 7 | input_strategy = stub_file_input_strategy 8 | interpreter = stub_interpreter 9 | env = double(:env) 10 | runner = described_class.new(env: env, path: 'my/path') 11 | 12 | runner.run 13 | 14 | expect(interpreter).to have_received(:run) 15 | expect(Gitsh::Interpreter).to have_received(:new).with( 16 | env: env, 17 | input_strategy: input_strategy, 18 | ) 19 | expect(Gitsh::InputStrategies::File).to have_received(:new).with( 20 | env: env, 21 | path: 'my/path', 22 | ) 23 | end 24 | end 25 | 26 | def stub_file_input_strategy 27 | input_strategy = double(:input_strategy) 28 | allow(Gitsh::InputStrategies::File).to receive(:new). 29 | and_return(input_strategy) 30 | input_strategy 31 | end 32 | 33 | def stub_interpreter 34 | interpreter = double(:interpreter, run: nil) 35 | allow(Gitsh::Interpreter).to receive(:new).and_return(interpreter) 36 | interpreter 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/units/git_command_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/git_command_list' 3 | 4 | describe Gitsh::GitCommandList do 5 | GIT_COMMAND = '/usr/bin/env git'.freeze 6 | MODERN_LIST_COMMAND = "#{GIT_COMMAND} --list-cmds=main,nohelpers".freeze 7 | MODERN_HELP_COMMAND = "#{GIT_COMMAND} help -a --no-verbose".freeze 8 | LEGACY_HELP_COMMAND = "#{GIT_COMMAND} help -a".freeze 9 | 10 | describe '#to_a' do 11 | it 'produces the list of porcelain commands' do 12 | commands = Gitsh::GitCommandList.new(env).to_a 13 | 14 | expect(commands).to include %(add) 15 | expect(commands).to include %(commit) 16 | expect(commands).to include %(checkout) 17 | expect(commands).to include %(status) 18 | expect(commands).not_to include %(add--interactive) 19 | expect(commands).not_to include '' 20 | end 21 | 22 | context 'with a Git version that supports --list-cmds' do 23 | it 'uses that command list' do 24 | stub_command(MODERN_LIST_COMMAND, output: "commit\nstatus\nadd\n") 25 | 26 | commands = Gitsh::GitCommandList.new(env).to_a 27 | 28 | expect(commands).to eq ['add', 'commit', 'status'] 29 | end 30 | end 31 | 32 | context 'with a Git version that supports `help --no-verbose`' do 33 | it 'parses the help output' do 34 | stub_command(MODERN_LIST_COMMAND, success: false) 35 | stub_command( 36 | MODERN_HELP_COMMAND, 37 | output: "Commands:\n commit status\n add\n", 38 | ) 39 | 40 | commands = Gitsh::GitCommandList.new(env).to_a 41 | 42 | expect(commands).to eq ['add', 'commit', 'status'] 43 | end 44 | end 45 | 46 | context 'with an old Git version' do 47 | it 'parses the help output' do 48 | stub_command(MODERN_LIST_COMMAND, success: false) 49 | stub_command(MODERN_HELP_COMMAND, success: false) 50 | stub_command( 51 | LEGACY_HELP_COMMAND, 52 | output: "Commands:\n commit status\n add\n", 53 | ) 54 | 55 | commands = Gitsh::GitCommandList.new(env).to_a 56 | 57 | expect(commands).to eq ['add', 'commit', 'status'] 58 | end 59 | end 60 | 61 | context 'when nothing we try works' do 62 | it 'returns an empty array' do 63 | stub_command(MODERN_LIST_COMMAND, success: false) 64 | stub_command(MODERN_HELP_COMMAND, success: false) 65 | stub_command(LEGACY_HELP_COMMAND, success: false) 66 | 67 | commands = Gitsh::GitCommandList.new(env).to_a 68 | 69 | expect(commands).to eq [] 70 | end 71 | end 72 | end 73 | 74 | def env 75 | double(git_command: GIT_COMMAND) 76 | end 77 | 78 | def stub_command(command, success: true, output: '') 79 | status = instance_double(Process::Status, success?: success) 80 | allow(Open3).to receive(:capture3).with(command). 81 | and_return([output, '', status]) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/units/git_repository/status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/git_repository/status' 3 | 4 | describe Gitsh::GitRepository::Status do 5 | describe '#initialized?' do 6 | it 'returns true when git directory exists' do 7 | repository_git_dir = File.expand_path('../../../../.git', __FILE__) 8 | status = Gitsh::GitRepository::Status.new('', repository_git_dir) 9 | expect(status).to be_initialized 10 | 11 | status = Gitsh::GitRepository::Status.new('', '/.git') 12 | expect(status).not_to be_initialized 13 | end 14 | end 15 | 16 | describe "#has_modified_files?" do 17 | it 'returns true when there are modified files in the repository' do 18 | status = Gitsh::GitRepository::Status.new( 19 | "?? example1.txt\n M example2.txt\n", 20 | '' 21 | ) 22 | expect(status).to have_modified_files 23 | 24 | status = Gitsh::GitRepository::Status.new( 25 | "?? example1.txt\n?? example2.txt", 26 | '' 27 | ) 28 | expect(status).not_to have_modified_files 29 | end 30 | end 31 | 32 | describe "#has_untracked_files?" do 33 | it 'returns true when there are untracked files in the repository' do 34 | status = Gitsh::GitRepository::Status.new( 35 | " M example1.txt\n?? example2.txt\n", 36 | '', 37 | ) 38 | expect(status).to have_untracked_files 39 | 40 | status = Gitsh::GitRepository::Status.new( 41 | " M example1.txt\n M example2.txt\n", 42 | '' 43 | ) 44 | expect(status).not_to have_untracked_files 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/units/history_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/history' 3 | 4 | describe Gitsh::History do 5 | before do 6 | @history_file = Tempfile.new('history') 7 | end 8 | 9 | after do 10 | @history_file.close 11 | @history_file.unlink 12 | end 13 | 14 | let(:env) { { 'gitsh.historyFile' => @history_file.path } } 15 | let(:line_editor) { 16 | Class.new.tap { |line_editor| line_editor::HISTORY = [] } 17 | } 18 | 19 | describe '#load' do 20 | it 'adds the saved history to the line editor' do 21 | write_history_file ['init', 'add -p', 'commit'] 22 | 23 | described_class.new(env, line_editor).load 24 | 25 | expect(line_editor::HISTORY).to eq ['init', 'add -p', 'commit'] 26 | end 27 | 28 | it 'does nothing when the history file does not exist' do 29 | history = described_class.new(env, line_editor) 30 | @history_file.close 31 | @history_file.unlink 32 | 33 | history.load 34 | 35 | expect(line_editor::HISTORY).to be_empty 36 | end 37 | end 38 | 39 | describe '#save' do 40 | it 'saves the history from the line editor to disk' do 41 | line_editor::HISTORY.concat(['init', 'add .', 'commit -m "Initial"']) 42 | 43 | described_class.new(env, line_editor).save 44 | 45 | expect(history_file_lines).to eq [ 46 | "init\n", "add .\n", "commit -m \"Initial\"\n" 47 | ] 48 | end 49 | 50 | it 'is limited by the gitsh.historySize setting' do 51 | line_editor::HISTORY.concat(['init', 'add .', 'commit -m "Initial"']) 52 | env['gitsh.historySize'] = 2 53 | 54 | described_class.new(env, line_editor).save 55 | 56 | expect(history_file_lines).to eq [ 57 | "add .\n", "commit -m \"Initial\"\n" 58 | ] 59 | end 60 | end 61 | 62 | def write_history_file(commands) 63 | commands.each do |command| 64 | @history_file.write("#{command}\n") 65 | end 66 | @history_file.rewind 67 | end 68 | 69 | def history_file_lines 70 | @history_file.each_line.to_a 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/units/lexer/character_class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/lexer/character_class' 3 | 4 | describe Gitsh::Lexer::CharacterClass do 5 | describe '#characters' do 6 | it 'returns the characters passed to the initializer' do 7 | chars = ['a', 'e', 'i', 'o', 'u'] 8 | vowels = described_class.new(chars) 9 | 10 | expect(vowels.characters).to eq chars 11 | end 12 | end 13 | 14 | describe '#+' do 15 | it 'returns a new character class combining two others' do 16 | a = described_class.new(['a']) 17 | b = described_class.new(['b']) 18 | 19 | result = a + b 20 | 21 | expect(result.characters).to eq ['a', 'b'] 22 | end 23 | end 24 | 25 | describe '#to_regexp' do 26 | it 'returns a Regexp matching the characters in the class' do 27 | vowels = described_class.new(['a', 'e', 'i', 'o', 'u']) 28 | 29 | regexp = vowels.to_regexp 30 | 31 | expect(regexp).to be_a Regexp 32 | expect(regexp).to match 'a' 33 | expect(regexp).not_to match 'b' 34 | end 35 | 36 | context 'with characters that need escaping in a Regexp' do 37 | it 'returns a properly escaped Regexp' do 38 | chars = described_class.new(['^', '$']) 39 | 40 | regexp = chars.to_regexp 41 | 42 | expect(regexp).to be_a Regexp 43 | expect(regexp).to match '^' 44 | expect(regexp).to match '$' 45 | expect(regexp).not_to match 'a' 46 | end 47 | end 48 | end 49 | 50 | describe '#to_negative_regexp' do 51 | it 'returns a Regexp excluding the characters in the class' do 52 | vowels = described_class.new(['a', 'e', 'i', 'o', 'u']) 53 | 54 | regexp = vowels.to_negative_regexp 55 | 56 | expect(regexp).to be_a Regexp 57 | expect(regexp).not_to match 'a' 58 | expect(regexp).to match 'b' 59 | end 60 | 61 | context 'with characters that need escaping in a Regexp' do 62 | it 'returns a properly escaped Regexp' do 63 | chars = described_class.new(['^', '$']) 64 | 65 | regexp = chars.to_negative_regexp 66 | 67 | expect(regexp).to be_a Regexp 68 | expect(regexp).not_to match '^' 69 | expect(regexp).not_to match '$' 70 | expect(regexp).to match 'a' 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/units/prompt_color_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/prompt_color' 3 | 4 | describe Gitsh::PromptColor do 5 | include Color 6 | 7 | describe '#status_color' do 8 | context 'with an uninitialized repo' do 9 | it 'uses the gitsh.color.uninitialized setting' do 10 | color = double('color') 11 | env = double('env', repo_config_color: color) 12 | prompt_color = described_class.new(env) 13 | status = double('status', initialized?: false) 14 | 15 | expect(prompt_color.status_color(status)).to eq color 16 | expect(env).to have_received(:repo_config_color). 17 | with('gitsh.color.uninitialized', 'normal red') 18 | end 19 | end 20 | 21 | context 'with untracked files' do 22 | it 'uses the gitsh.color.untracked setting' do 23 | color = double('color') 24 | env = double('env', repo_config_color: color) 25 | status = double( 26 | 'status', 27 | initialized?: true, 28 | has_untracked_files?: true, 29 | ) 30 | prompt_color = described_class.new(env) 31 | 32 | expect(prompt_color.status_color(status)).to eq color 33 | expect(env).to have_received(:repo_config_color). 34 | with('gitsh.color.untracked', 'red') 35 | end 36 | end 37 | 38 | context 'with modified files' do 39 | it 'uses the gitsh.color.modified setting' do 40 | color = double('color') 41 | env = double('env', repo_config_color: color) 42 | status = double( 43 | 'status', 44 | initialized?: true, 45 | has_untracked_files?: false, 46 | has_modified_files?: true, 47 | ) 48 | prompt_color = described_class.new(env) 49 | 50 | expect(prompt_color.status_color(status)).to eq color 51 | expect(env).to have_received(:repo_config_color). 52 | with('gitsh.color.modified', 'yellow') 53 | end 54 | end 55 | 56 | context 'with a clean repo' do 57 | it 'uses the gitsh.color.default setting' do 58 | color = double('color') 59 | env = double('env', repo_config_color: color) 60 | status = double( 61 | 'status', 62 | initialized?: true, 63 | has_untracked_files?: false, 64 | has_modified_files?: false, 65 | ) 66 | prompt_color = described_class.new(env) 67 | 68 | expect(prompt_color.status_color(status)).to eq color 69 | expect(env).to have_received(:repo_config_color). 70 | with('gitsh.color.default', 'blue') 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/units/quote_detector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/quote_detector' 3 | 4 | describe Gitsh::QuoteDetector do 5 | describe '#call' do 6 | context 'for an escaped character' do 7 | it 'returns true' do 8 | expect(detect('a\\ b', 2)).to be true 9 | expect(detect('\\ b', 1)).to be true 10 | end 11 | end 12 | 13 | context 'for an unescaped character' do 14 | it 'returns false' do 15 | expect(detect('a b', 1)).to be false 16 | expect(detect(' b', 0)).to be false 17 | end 18 | end 19 | 20 | context 'for repeated escape characters' do 21 | it 'returns true for odd numbers' do 22 | expect(detect('a\\\\ b', 3)).to be false 23 | expect(detect('a\\\\\\ b', 4)).to be true 24 | expect(detect('a\\\\\\\\ b', 5)).to be false 25 | 26 | expect(detect('\\\\ b', 2)).to be false 27 | expect(detect('\\\\\\ b', 3)).to be true 28 | expect(detect('\\\\\\\\ b', 4)).to be false 29 | end 30 | end 31 | end 32 | 33 | def detect(text, index) 34 | described_class.new.call(text, index) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/units/shell_command_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/shell_command_runner' 3 | 4 | describe Gitsh::ShellCommandRunner do 5 | describe '#run' do 6 | before do 7 | allow(Process).to receive(:spawn).and_return(1) 8 | allow(Process).to receive(:wait) 9 | allow(Process).to receive(:kill) 10 | ensure_exit_status_exists 11 | end 12 | 13 | it 'spawns a process with the command and arguments' do 14 | runner = described_class.new(["echo", "Hello world"], env) 15 | 16 | runner.run 17 | 18 | expect(Process).to have_received(:spawn).with( 19 | 'echo', 'Hello world', 20 | out: env.output_stream.to_i, 21 | err: env.error_stream.to_i 22 | ) 23 | end 24 | 25 | it 'returns true when the shell command succeeds' do 26 | allow($?).to receive(:success?).and_return(true) 27 | runner = described_class.new(['goodcommand'], env) 28 | 29 | expect(runner.run).to eq true 30 | end 31 | 32 | it 'returns false when the shell command fails' do 33 | allow($?).to receive(:success?).and_return(false) 34 | runner = described_class.new(['badcommand'], env) 35 | 36 | expect(runner.run).to eq false 37 | end 38 | 39 | it 'returns false when Process.spawn raises' do 40 | allow(Process).to receive(:spawn).and_raise(Errno::ENOENT, 'No such file') 41 | runner = described_class.new(['badcommand'], env) 42 | 43 | expect(runner.run).to eq false 44 | end 45 | 46 | it 'forwards interrupts to the child process' do 47 | pid = 12 48 | wait_results = StubbedMethodResult.new. 49 | raises(Interrupt). 50 | returns(nil) 51 | allow(Process).to receive(:spawn).and_return(pid) 52 | allow(Process).to receive(:wait).with(pid) { wait_results.next_result } 53 | runner = described_class.new(["vim"], env) 54 | 55 | runner.run 56 | 57 | expect(Process).to have_received(:wait).with(pid).twice 58 | expect(Process).to have_received(:kill).with('INT', pid).once 59 | end 60 | end 61 | 62 | def ensure_exit_status_exists 63 | `pwd` 64 | expect($?).not_to be_nil 65 | end 66 | 67 | let(:env) do 68 | double('Environment', 69 | output_stream: double('OutputStream', to_i: 1), 70 | error_stream: double('ErrorStream', to_i: 2), 71 | puts_error: nil 72 | ) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/units/tab_completion/alias_expander_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/alias_expander' 3 | 4 | describe Gitsh::TabCompletion::AliasExpander do 5 | describe '#call' do 6 | context 'when the first word is an alias for a Git command' do 7 | it 'expands the alias' do 8 | env = double(:env) 9 | allow(env).to receive(:fetch).with('alias.alias'). 10 | and_return('expanded command') 11 | 12 | expect(expand(['alias', 'argument'], env)). 13 | to eq ['expanded', 'command', 'argument'] 14 | end 15 | end 16 | 17 | context 'when the first word is an alias for a shell command' do 18 | it 'does not expand the alias' do 19 | env = double(:env) 20 | allow(env).to receive(:fetch).with('alias.alias'). 21 | and_return('!shell command') 22 | 23 | expect(expand(['alias', 'argument'], env)). 24 | to eq ['alias', 'argument'] 25 | end 26 | end 27 | 28 | context 'when the first word is not an alias' do 29 | it 'returns the words' do 30 | env = double(:env) 31 | allow(env).to receive(:fetch).with('alias.foo'). 32 | and_raise(Gitsh::UnsetVariableError) 33 | words = ['foo', 'bar'] 34 | 35 | expect(expand(words, env)).to eq(words) 36 | end 37 | end 38 | end 39 | 40 | def expand(words, env) 41 | Gitsh::TabCompletion::AliasExpander.new(words, env).call 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/units/tab_completion/automaton_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/automaton_factory' 3 | 4 | describe Gitsh::TabCompletion::AutomatonFactory do 5 | describe '.build' do 6 | it 'loads the tab completion DSL file' do 7 | config_directory = '/tmp/etc/gitsh' 8 | env = double(:env, config_directory: config_directory) 9 | start_state = stub_automaton_state 10 | automaton = stub_automaton 11 | stub_dsl_loading 12 | config_path = File.join(config_directory, 'completions') 13 | 14 | result = described_class.build(env) 15 | 16 | expect(result).to eq(automaton) 17 | expect(Gitsh::TabCompletion::Automaton). 18 | to have_received(:new).with(start_state) 19 | expect(Gitsh::TabCompletion::DSL). 20 | to have_received(:load).with(config_path, start_state, env) 21 | end 22 | end 23 | 24 | def stub_automaton_state 25 | stub_class(Gitsh::TabCompletion::Automaton::State) 26 | end 27 | 28 | def stub_automaton 29 | stub_class(Gitsh::TabCompletion::Automaton) 30 | end 31 | 32 | def stub_dsl_loading 33 | allow(Gitsh::TabCompletion::DSL).to receive(:load) 34 | end 35 | 36 | def stub_class(klass) 37 | instance = instance_double(klass) 38 | allow(klass).to receive(:new).and_return(instance) 39 | instance 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/units/tab_completion/context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/context' 3 | 4 | describe Gitsh::TabCompletion::Context do 5 | describe '#prior_words' do 6 | it 'produces the words in the input before the word being completed' do 7 | context = described_class.new('stash drop my-') 8 | expect(context.prior_words).to eq %w(stash drop) 9 | end 10 | 11 | it 'includes variables' do 12 | context = described_class.new(':echo "name=$user.name" "email=') 13 | expect(context.prior_words).to eq [':echo', 'name=${user.name}'] 14 | end 15 | 16 | it 'only considers the current command' do 17 | context = described_class.new('stash apply my-stash && stash drop my-') 18 | expect(context.prior_words).to eq %w(stash drop) 19 | end 20 | 21 | it 'handles multiple lines' do 22 | context = described_class.new("(add .\ncommit -") 23 | expect(context.prior_words).to eq %w(commit) 24 | end 25 | 26 | it 'handles partially quoted words' do 27 | context = described_class.new('sta"sh" drop my-') 28 | expect(context.prior_words).to eq %w(stash drop) 29 | end 30 | 31 | it 'only considers the current subshell' do 32 | context = described_class.new(':echo $(config ') 33 | expect(context.prior_words).to eq %w(config) 34 | end 35 | 36 | it 'only considers the current parenthetical' do 37 | context = described_class.new('(config ') 38 | expect(context.prior_words).to eq %w(config) 39 | end 40 | 41 | context 'with input the Lexer cannot handle' do 42 | it 'does not explode' do 43 | allow(Gitsh::Lexer). 44 | to receive(:lex).and_raise(RLTK::LexingError.new(0, 1, 0, nil)) 45 | 46 | expect(described_class.new('bad input').prior_words).to eq [] 47 | end 48 | end 49 | end 50 | 51 | describe '#completing_variable?' do 52 | it 'returns true when the command ends with a variable' do 53 | expect(described_class.new(':echo $my_va')).to be_completing_variable 54 | expect(described_class.new(':echo "$my_va')).to be_completing_variable 55 | expect(described_class.new(':echo ${my_va')).to be_completing_variable 56 | expect(described_class.new(':echo $')).to be_completing_variable 57 | expect(described_class.new(':echo ${')).to be_completing_variable 58 | end 59 | 60 | it 'returns false when the command does not end with a variable' do 61 | expect(described_class.new(':echo hello')).not_to be_completing_variable 62 | expect(described_class.new(':echo $my_var ')).not_to be_completing_variable 63 | expect(described_class.new(':echo \'$varish')).not_to be_completing_variable 64 | expect(described_class.new(':echo \'$')).not_to be_completing_variable 65 | expect(described_class.new('')).not_to be_completing_variable 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/choice_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/choice_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::ChoiceFactory do 5 | describe '#build' do 6 | it 'applies all of the choice factories to the same end state' do 7 | start_state = double(:start_state) 8 | first_choice = double(:first_choice, build: nil) 9 | second_choice = double(:second_choice, build: nil) 10 | factory = described_class.new([first_choice, second_choice]) 11 | 12 | end_state = factory.build(start_state, option: 'baz') 13 | 14 | expect(end_state).to be_a(Gitsh::TabCompletion::Automaton::State) 15 | expect(first_choice).to have_received(:build).with( 16 | start_state, 17 | end_state: end_state, 18 | option: 'baz', 19 | ) 20 | expect(second_choice).to have_received(:build).with( 21 | start_state, 22 | end_state: end_state, 23 | option: 'baz', 24 | ) 25 | end 26 | 27 | context 'given an end state' do 28 | it 'applies all of the choice factories to the given end state' do 29 | start_state = double(:start_state) 30 | end_state = double(:end_state) 31 | first_choice = double(:first_choice, build: nil) 32 | second_choice = double(:second_choice, build: nil) 33 | factory = described_class.new([first_choice, second_choice]) 34 | 35 | result = factory.build(start_state, option: 'baz', end_state: end_state) 36 | 37 | expect(result).to eq(end_state) 38 | expect(first_choice).to have_received(:build).with( 39 | start_state, 40 | end_state: end_state, 41 | option: 'baz', 42 | ) 43 | expect(second_choice).to have_received(:build).with( 44 | start_state, 45 | end_state: end_state, 46 | option: 'baz', 47 | ) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/concatenation_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/concatenation_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::ConcatenationFactory do 5 | describe '#build' do 6 | it 'connects the various parts in a sequence, and returns the end state' do 7 | start_state = double(:start_state) 8 | end_state_1 = double(:end_state_1) 9 | part_1 = double(:factory, build: end_state_1) 10 | end_state_2 = double(:end_state_2) 11 | part_2 = double(:factory, build: end_state_2) 12 | factory = described_class.new([part_1, part_2]) 13 | 14 | result = factory.build(start_state, option: 'foo') 15 | 16 | expect(part_1).to have_received(:build). 17 | with(start_state, option: 'foo') 18 | expect(part_2).to have_received(:build). 19 | with(end_state_1, option: 'foo') 20 | expect(result).to eq(end_state_2) 21 | end 22 | 23 | context 'given an explicit end state' do 24 | it 'connects the various parts in a sequence ending at the end state' do 25 | start_state = double(:start_state) 26 | end_state = double(:end_state) 27 | end_state_1 = double(:end_state_1) 28 | part_1 = double(:factory, build: end_state_1) 29 | end_state_2 = double(:end_state_2, add_free_transition: nil) 30 | part_2 = double(:factory, build: end_state_2) 31 | factory = described_class.new([part_1, part_2]) 32 | 33 | result = factory.build(start_state, option: 'foo', end_state: end_state) 34 | 35 | expect(part_1).to have_received(:build). 36 | with(start_state, option: 'foo') 37 | expect(part_2).to have_received(:build). 38 | with(end_state_1, option: 'foo') 39 | expect(end_state_2).to have_received(:add_free_transition). 40 | with(end_state) 41 | expect(result).to eq(end_state) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/fallback_transition_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/fallback_transition_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::FallbackTransitionFactory do 5 | describe '#build' do 6 | it 'adds a fallback transition to the start state and returns the end state' do 7 | start_state = double(:start_state, add_fallback_transition: nil) 8 | matcher = double(:matcher) 9 | factory = described_class.new(matcher) 10 | 11 | end_state = factory.build(start_state) 12 | 13 | expect(end_state).to be_a(Gitsh::TabCompletion::Automaton::State) 14 | expect(start_state). 15 | to have_received(:add_fallback_transition).with(matcher, end_state) 16 | end 17 | 18 | context 'given an end state' do 19 | it 'adds a fallback transition between the start and end states' do 20 | start_state = double(:start_state, add_fallback_transition: nil) 21 | end_state = double(:end_state) 22 | matcher = double(:matcher) 23 | factory = described_class.new(matcher) 24 | 25 | result = factory.build(start_state, end_state: end_state) 26 | 27 | expect(result).to eq(end_state) 28 | expect(start_state). 29 | to have_received(:add_fallback_transition).with(matcher, end_state) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/maybe_operation_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/maybe_operation_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::MaybeOperationFactory do 5 | describe '#build' do 6 | it 'calls the child factory and adds a free transition' do 7 | start_state = double(:start_state, add_free_transition: nil) 8 | end_state = double(:end_state) 9 | child = double(:factory, build: end_state) 10 | factory = described_class.new(child) 11 | 12 | result = factory.build(start_state, answer: 42) 13 | 14 | expect(result).to eq(end_state) 15 | expect(child).to have_received(:build).with(start_state, answer: 42) 16 | expect(start_state).to have_received(:add_free_transition).with(end_state) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/option_transition_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/option_transition_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::OptionTransitionFactory do 5 | describe '#build' do 6 | it 'adds transitions for known and unknown options' do 7 | unknown_option_matcher = stub_unknown_option_matcher 8 | start_state = double(:start_state, add_transition: nil) 9 | known_options_factory = double(:factory, build: nil) 10 | factory = described_class.new 11 | 12 | end_state = factory.build( 13 | start_state, 14 | known_options_factory: known_options_factory, 15 | ) 16 | 17 | expect(known_options_factory).to have_received(:build).with( 18 | start_state, 19 | known_options_factory: known_options_factory, 20 | end_state: end_state, 21 | ) 22 | expect(start_state).to have_received(:add_transition).with( 23 | unknown_option_matcher, 24 | end_state, 25 | ) 26 | end 27 | 28 | context 'with an explicit end state' do 29 | it 'adds transitions for known and unknown options' do 30 | unknown_option_matcher = stub_unknown_option_matcher 31 | start_state = double(:start_state, add_transition: nil) 32 | end_state = double(:end_state) 33 | known_options_factory = double(:factory, build: nil) 34 | factory = described_class.new 35 | 36 | result = factory.build( 37 | start_state, 38 | known_options_factory: known_options_factory, 39 | end_state: end_state, 40 | ) 41 | 42 | expect(result).to eq(end_state) 43 | expect(known_options_factory).to have_received(:build).with( 44 | start_state, 45 | known_options_factory: known_options_factory, 46 | end_state: end_state, 47 | ) 48 | expect(start_state).to have_received(:add_transition).with( 49 | unknown_option_matcher, 50 | end_state, 51 | ) 52 | end 53 | end 54 | end 55 | 56 | def stub_unknown_option_matcher 57 | klass = Gitsh::TabCompletion::Matchers::UnknownOptionMatcher 58 | matcher = instance_double(klass) 59 | allow(klass).to receive(:new).and_return(matcher) 60 | matcher 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/parse_error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rltk' 3 | require 'gitsh/tab_completion/dsl/parse_error' 4 | 5 | describe Gitsh::TabCompletion::DSL::ParseError do 6 | describe '#to_s' do 7 | it 'describes the token where the problem occurred' do 8 | position = instance_double( 9 | RLTK::StreamPosition, 10 | line_number: 2, 11 | line_offset: 3, 12 | file_name: 'example.txt', 13 | ) 14 | token = instance_double( 15 | RLTK::Token, 16 | type: :MAYBE, 17 | position: position, 18 | value: nil, 19 | ) 20 | exception = described_class.new('Unexpected', token) 21 | 22 | expect(exception.to_s).to eq( 23 | 'Tab completion configuration error: Unexpected operator (?) '\ 24 | 'at line 2, column 4 in file example.txt' 25 | ) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/plus_operation_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/plus_operation_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::PlusOperationFactory do 5 | describe '#build' do 6 | it 'calls the child factory and adds a free transition' do 7 | start_state = double(:start_state) 8 | end_state = double(:end_state, add_free_transition: nil) 9 | child = double(:factory, build: end_state) 10 | factory = described_class.new(child) 11 | 12 | result = factory.build(start_state, option: 'bar') 13 | 14 | expect(result).to eq(end_state) 15 | expect(child).to have_received(:build).with(start_state, option: 'bar') 16 | expect(end_state).to have_received(:add_free_transition).with(start_state) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/rule_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/rule_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::RuleFactory do 5 | describe '#build' do 6 | it "delegates to the rule's root factory" do 7 | start_state = double(:start_state) 8 | root = double(:factory, build: nil) 9 | options = double(:options) 10 | factory = described_class.new(root, options) 11 | 12 | factory.build(start_state) 13 | 14 | expect(root).to have_received(:build). 15 | with(start_state, known_options_factory: options) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/rule_set_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/rule_set_factory' 3 | require 'gitsh/tab_completion/dsl/rule_factory' 4 | 5 | describe Gitsh::TabCompletion::DSL::RuleSetFactory do 6 | describe '#build' do 7 | it 'delegates to the various rule factories' do 8 | start_state = double(:start_state) 9 | rule_factory_1 = stub_rule_factory 10 | rule_factory_2 = stub_rule_factory 11 | factory = described_class.new([rule_factory_1, rule_factory_2]) 12 | 13 | factory.build(start_state) 14 | 15 | expect(rule_factory_1).to have_received(:build).with(start_state).once 16 | expect(rule_factory_2).to have_received(:build).with(start_state).once 17 | end 18 | end 19 | 20 | def stub_rule_factory 21 | instance_double( 22 | Gitsh::TabCompletion::DSL::RuleFactory, 23 | build: nil, 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/star_operation_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/star_operation_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::StarOperationFactory do 5 | describe '#build' do 6 | it 'calls the child factory with the start state as the end state' do 7 | start_state = double(:start_state) 8 | child_result = double(:child_result) 9 | child = double(:factory, build: child_result) 10 | factory = described_class.new(child) 11 | 12 | result = factory.build(start_state, option: 'foo') 13 | 14 | expect(result).to eq child_result 15 | expect(child).to have_received(:build).with( 16 | start_state, 17 | end_state: start_state, 18 | option: 'foo', 19 | ) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/text_transition_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/text_transition_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::TextTransitionFactory do 5 | describe '#build' do 6 | it 'adds a transition to the start state and returns the end state' do 7 | start_state = double(:start_state, add_transition: nil) 8 | matcher = stub_text_matcher('commit') 9 | factory = described_class.new('commit') 10 | 11 | end_state = factory.build(start_state) 12 | 13 | expect(end_state).to be_a(Gitsh::TabCompletion::Automaton::State) 14 | expect(start_state). 15 | to have_received(:add_transition).with(matcher, end_state) 16 | end 17 | 18 | context 'given an end state' do 19 | it 'adds a transition between the start and end states' do 20 | start_state = double(:start_state, add_transition: nil) 21 | end_state = double(:end_state) 22 | matcher = stub_text_matcher('commit') 23 | factory = described_class.new('commit') 24 | 25 | result = factory.build(start_state, end_state: end_state) 26 | 27 | expect(result).to eq(end_state) 28 | expect(start_state). 29 | to have_received(:add_transition).with(matcher, end_state) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl/variable_transition_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl/variable_transition_factory' 3 | 4 | describe Gitsh::TabCompletion::DSL::VariableTransitionFactory do 5 | describe '#build' do 6 | it 'adds a transition to the start state and returns the end state' do 7 | matcher = double(:matcher, name: 'my-matcher') 8 | start_state = double(:start_state, add_transition: nil) 9 | factory = described_class.new(matcher) 10 | 11 | end_state = factory.build(start_state) 12 | 13 | expect(end_state).to be_a(Gitsh::TabCompletion::Automaton::State) 14 | expect(start_state). 15 | to have_received(:add_transition).with(matcher, end_state) 16 | end 17 | 18 | context 'given an end state' do 19 | it 'adds a transition between the start and end states' do 20 | matcher = double(:matcher, name: 'my-matcher') 21 | start_state = double(:start_state, add_transition: nil) 22 | end_state = double(:end_state) 23 | factory = described_class.new(matcher) 24 | 25 | result = factory.build(start_state, end_state: end_state) 26 | 27 | expect(result).to eq(end_state) 28 | expect(start_state). 29 | to have_received(:add_transition).with(matcher, end_state) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/units/tab_completion/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/dsl' 3 | 4 | describe Gitsh::TabCompletion::DSL do 5 | describe '.load' do 6 | it 'builds a graph of automaton states by reading the given file' do 7 | path = write_temp_completions_file([ 8 | 'stash (apply|drop|pop|show)', 9 | ].join("\n")) 10 | start_state = Gitsh::TabCompletion::Automaton::State.new('start') 11 | env = Gitsh::Environment.new 12 | 13 | described_class.load(path, start_state, env) 14 | 15 | automaton = Gitsh::TabCompletion::Automaton.new(start_state) 16 | expect(automaton.completions([], '')). 17 | to match_array ['stash'] 18 | expect(automaton.completions(['stash'], '')). 19 | to match_array ['apply', 'drop', 'pop', 'show'] 20 | end 21 | 22 | context 'with a path to a file that does not exist' do 23 | it 'does not explode' do 24 | path = '/not/a/real/path' 25 | start_state = Gitsh::TabCompletion::Automaton::State.new('start') 26 | env = Gitsh::Environment.new 27 | 28 | expect { described_class.load(path, start_state, env) }. 29 | not_to raise_exception 30 | end 31 | end 32 | end 33 | 34 | def write_temp_completions_file(completions) 35 | temp_file('gitsh_completions', completions).path 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/units/tab_completion/facade_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/error' 3 | require 'gitsh/tab_completion/facade' 4 | 5 | describe Gitsh::TabCompletion::Facade do 6 | describe '#call' do 7 | context 'given input not ending with a variable' do 8 | it 'invokes the CommandCompleter' do 9 | input = 'add -p $path lib/' 10 | line_editor = double(:line_editor, line_buffer: input) 11 | command_completer = stub_command_completer 12 | stub_variable_completer 13 | automaton = stub_automaton_factory 14 | escaper = stub_escaper 15 | facade = described_class.new(line_editor, stub_env) 16 | 17 | facade.call('lib/') 18 | 19 | expect(Gitsh::TabCompletion::CommandCompleter).to have_received(:new).with( 20 | line_editor, 21 | ['add', '-p', '${path}'], 22 | 'lib/', 23 | automaton, 24 | escaper, 25 | ) 26 | expect(command_completer).to have_received(:call) 27 | expect(Gitsh::TabCompletion::VariableCompleter). 28 | not_to have_received(:new) 29 | end 30 | end 31 | 32 | context 'given input ending with a variable' do 33 | it 'invokes the VariableCompleter' do 34 | input = ':echo "name=$g' 35 | line_editor = double(:line_editor, line_buffer: input) 36 | stub_command_completer 37 | variable_completer = stub_variable_completer 38 | env = double(:env, config_directory: '/tmp/gitsh/') 39 | facade = described_class.new(line_editor, env) 40 | 41 | facade.call('name=$g') 42 | 43 | expect(Gitsh::TabCompletion::VariableCompleter).to have_received(:new).with( 44 | line_editor, 45 | 'name=$g', 46 | env, 47 | ) 48 | expect(variable_completer).to have_received(:call) 49 | expect(Gitsh::TabCompletion::CommandCompleter). 50 | not_to have_received(:new) 51 | end 52 | end 53 | end 54 | 55 | def stub_command_completer 56 | stub_class(Gitsh::TabCompletion::CommandCompleter).tap do |completer| 57 | allow(completer).to receive(:call) 58 | end 59 | end 60 | 61 | def stub_variable_completer 62 | stub_class(Gitsh::TabCompletion::VariableCompleter).tap do |completer| 63 | allow(completer).to receive(:call) 64 | end 65 | end 66 | 67 | def stub_automaton_factory 68 | stub_class(Gitsh::TabCompletion::AutomatonFactory, :build) 69 | end 70 | 71 | def stub_escaper 72 | stub_class(Gitsh::TabCompletion::Escaper) 73 | end 74 | 75 | def stub_class(klass, method = :new) 76 | command_completer = instance_double(klass) 77 | allow(klass).to receive(method).and_return(command_completer) 78 | command_completer 79 | end 80 | 81 | def stub_env 82 | env = double(:env) 83 | allow(env).to receive(:fetch).and_raise(Gitsh::UnsetVariableError) 84 | env 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/anything_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/anything_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::AnythingMatcher do 5 | describe '#match?' do 6 | it 'returns true for all input' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('--all')).to be_truthy 11 | expect(matcher.match?('')).to be_truthy 12 | end 13 | end 14 | 15 | describe '#completions' do 16 | it 'returns an empty array' do 17 | matcher = described_class.new(double(:env)) 18 | 19 | expect(matcher.completions('foo')).to eq([]) 20 | expect(matcher.completions('--all')).to eq([]) 21 | expect(matcher.completions('')).to eq([]) 22 | end 23 | end 24 | 25 | describe '#eql?' do 26 | it 'returns true when given another instance of the same class' do 27 | matcher1 = described_class.new(double(:env)) 28 | matcher2 = described_class.new(double(:env)) 29 | 30 | expect(matcher1).to eql(matcher2) 31 | end 32 | 33 | it 'returns false when given an instance of any other class' do 34 | matcher = described_class.new(double(:env)) 35 | other = double(:not_a_matcher) 36 | 37 | expect(matcher).not_to eql(other) 38 | end 39 | end 40 | 41 | describe '#hash' do 42 | it 'returns the same value for all instances of the class' do 43 | matcher1 = described_class.new(double(:env)) 44 | matcher2 = described_class.new(double(:env)) 45 | 46 | expect(matcher1.hash).to eq(matcher2.hash) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/branch_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/branch_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::BranchMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | context 'given blank input' do 16 | it 'returns the names of all branches' do 17 | env = double(:env, repo_branches: ['master', 'my-feature']) 18 | matcher = described_class.new(env) 19 | 20 | expect(matcher.completions('')).to match_array ['master', 'my-feature'] 21 | end 22 | end 23 | 24 | context 'given a partial branch name' do 25 | it 'returns all branch names matching the input' do 26 | env = double(:env, repo_branches: ['master', 'my-feature']) 27 | matcher = described_class.new(env) 28 | 29 | expect(matcher.completions('m')). 30 | to match_array ['master', 'my-feature'] 31 | expect(matcher.completions('my')). 32 | to match_array ['my-feature'] 33 | expect(matcher.completions('foo')). 34 | to match_array [] 35 | end 36 | end 37 | end 38 | 39 | describe '#eql?' do 40 | it 'returns true when given another instance of the same class' do 41 | env = double(:env) 42 | matcher1 = described_class.new(env) 43 | matcher2 = described_class.new(env) 44 | 45 | expect(matcher1).to eql(matcher2) 46 | end 47 | 48 | it 'returns false when given an instance of any other class' do 49 | matcher = described_class.new(double(:env)) 50 | other = double(:not_a_matcher) 51 | 52 | expect(matcher).not_to eql(other) 53 | end 54 | end 55 | 56 | describe '#hash' do 57 | it 'returns the same value for all instances of the class' do 58 | env = double(:env) 59 | matcher1 = described_class.new(env) 60 | matcher2 = described_class.new(env) 61 | 62 | expect(matcher1.hash).to eq(matcher2.hash) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/command_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/command_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::CommandMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env), double(:internal_command)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | it 'returns the available commands (Git, internal, and aliases)' do 16 | env = double( 17 | :env, 18 | git_commands: ['add', 'commit'], 19 | git_aliases: ['graph', 'force'], 20 | ) 21 | internal_command = double( 22 | :internal_command, 23 | commands: [':echo', ':help'], 24 | ) 25 | matcher = described_class.new(env, internal_command) 26 | 27 | expect(matcher.completions('')).to match_array [ 28 | 'add', 'commit', 29 | 'graph', 'force', 30 | ':echo', ':help', 31 | ] 32 | end 33 | 34 | it 'filters the results based on the input' do 35 | env = double( 36 | :env, 37 | git_commands: ['add', 'grep'], 38 | git_aliases: ['graph', 'force'], 39 | ) 40 | internal_command = double( 41 | :internal_command, 42 | commands: [':echo', ':help'], 43 | ) 44 | matcher = described_class.new(env, internal_command) 45 | 46 | expect(matcher.completions('gr')).to match_array [ 47 | 'graph', 'grep', 48 | ] 49 | end 50 | end 51 | 52 | describe '#eql?' do 53 | it 'returns true when given another instance of the same class' do 54 | env = double(:env) 55 | internal_command = double(:internal_command) 56 | matcher1 = described_class.new(env, internal_command) 57 | matcher2 = described_class.new(env, internal_command) 58 | 59 | expect(matcher1).to eql(matcher2) 60 | end 61 | 62 | it 'returns false when given an instance of any other class' do 63 | matcher = described_class.new(double(:env), double(:internal_command)) 64 | other = double(:not_a_matcher) 65 | 66 | expect(matcher).not_to eql(other) 67 | end 68 | end 69 | 70 | describe '#hash' do 71 | it 'returns the same value for all instances of the class' do 72 | env = double(:env) 73 | internal_command = double(:internal_command) 74 | matcher1 = described_class.new(env, internal_command) 75 | matcher2 = described_class.new(env, internal_command) 76 | 77 | expect(matcher1.hash).to eq(matcher2.hash) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/path_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/path_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::PathMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | it 'returns paths based on the input' do 16 | in_a_temporary_directory do 17 | make_directory('foo') 18 | write_file('foo/first.txt') 19 | write_file('foo/second.txt') 20 | write_file('first.txt') 21 | matcher = described_class.new(double(:env)) 22 | 23 | expect(matcher.completions('')).to match_array ['foo/', 'first.txt'] 24 | expect(matcher.completions('f')).to match_array ['foo/', 'first.txt'] 25 | expect(matcher.completions('foo')).to match_array ['foo/'] 26 | expect(matcher.completions('foo/')). 27 | to match_array ['foo/first.txt', 'foo/second.txt'] 28 | end 29 | end 30 | end 31 | 32 | describe '#eql?' do 33 | it 'returns true when given another instance of the same class' do 34 | matcher1 = described_class.new(double(:env)) 35 | matcher2 = described_class.new(double(:env)) 36 | 37 | expect(matcher1).to eql(matcher2) 38 | end 39 | 40 | it 'returns false when given an instance of any other class' do 41 | matcher = described_class.new(double(:env)) 42 | other = double(:not_a_matcher) 43 | 44 | expect(matcher).not_to eql(other) 45 | end 46 | end 47 | 48 | describe '#hash' do 49 | it 'returns the same value for all instances of the class' do 50 | matcher1 = described_class.new(double(:env)) 51 | matcher2 = described_class.new(double(:env)) 52 | 53 | expect(matcher1.hash).to eq(matcher2.hash) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/remote_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/remote_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::RemoteMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | it 'returns the available Git remotes' do 16 | env = double(:env, repo_remotes: ['origin', 'github']) 17 | matcher = described_class.new(env) 18 | 19 | expect(matcher.completions('')). 20 | to match_array ['origin', 'github'] 21 | end 22 | 23 | it 'filters the results based on the input' do 24 | env = double(:env, repo_remotes: ['origin', 'github']) 25 | matcher = described_class.new(env) 26 | 27 | expect(matcher.completions('g')).to match_array ['github'] 28 | end 29 | end 30 | 31 | describe '#eql?' do 32 | it 'returns true when given another instance of the same class' do 33 | env = double(:env) 34 | matcher1 = described_class.new(env) 35 | matcher2 = described_class.new(env) 36 | 37 | expect(matcher1).to eql(matcher2) 38 | end 39 | 40 | it 'returns false when given an instance of any other class' do 41 | matcher = described_class.new(double(:env)) 42 | other = double(:not_a_matcher) 43 | 44 | expect(matcher).not_to eql(other) 45 | end 46 | end 47 | 48 | describe '#hash' do 49 | it 'returns the same value for all instances of the class' do 50 | env = double(:env) 51 | matcher1 = described_class.new(env) 52 | matcher2 = described_class.new(env) 53 | 54 | expect(matcher1.hash).to eq(matcher2.hash) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/revision_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/revision_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::RevisionMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | context 'given blank input' do 16 | it 'returns the names of all heads' do 17 | env = double(:env, repo_heads: ['master', 'my-feature']) 18 | matcher = described_class.new(env) 19 | 20 | expect(matcher.completions('')).to match_array ['master', 'my-feature'] 21 | end 22 | end 23 | 24 | context 'given input containing a prefix' do 25 | it 'returns the name of all heads with the prefix added' do 26 | env = double(:env, repo_heads: ['master', 'my-feature']) 27 | matcher = described_class.new(env) 28 | 29 | expect(matcher.completions('master..')). 30 | to match_array ['master..master', 'master..my-feature'] 31 | expect(matcher.completions('HEAD:')). 32 | to match_array ['HEAD:master', 'HEAD:my-feature'] 33 | end 34 | end 35 | 36 | context 'given a partial revision name' do 37 | it 'returns all heads matching the input' do 38 | env = double(:env, repo_heads: ['master', 'my-feature']) 39 | matcher = described_class.new(env) 40 | 41 | expect(matcher.completions('m')). 42 | to match_array ['master', 'my-feature'] 43 | expect(matcher.completions('my')). 44 | to match_array ['my-feature'] 45 | expect(matcher.completions('HEAD:m')). 46 | to match_array ['HEAD:master', 'HEAD:my-feature'] 47 | expect(matcher.completions('HEAD:my')). 48 | to match_array ['HEAD:my-feature'] 49 | expect(matcher.completions('HEAD:foo')). 50 | to match_array [] 51 | end 52 | end 53 | end 54 | 55 | describe '#eql?' do 56 | it 'returns true when given another instance of the same class' do 57 | env = double(:env) 58 | matcher1 = described_class.new(env) 59 | matcher2 = described_class.new(env) 60 | 61 | expect(matcher1).to eql(matcher2) 62 | end 63 | 64 | it 'returns false when given an instance of any other class' do 65 | matcher = described_class.new(double(:env)) 66 | other = double(:not_a_matcher) 67 | 68 | expect(matcher).not_to eql(other) 69 | end 70 | end 71 | 72 | describe '#hash' do 73 | it 'returns the same value for all instances of the class' do 74 | env = double(:env) 75 | matcher1 = described_class.new(env) 76 | matcher2 = described_class.new(env) 77 | 78 | expect(matcher1.hash).to eq(matcher2.hash) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/tag_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/tag_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::TagMatcher do 5 | describe '#match?' do 6 | it 'always returns true' do 7 | matcher = described_class.new(double(:env)) 8 | 9 | expect(matcher.match?('foo')).to be_truthy 10 | expect(matcher.match?('')).to be_truthy 11 | end 12 | end 13 | 14 | describe '#completions' do 15 | context 'given blank input' do 16 | it 'returns the names of all tags' do 17 | env = double(:env, repo_tags: ['v1.0', 'v1.1']) 18 | matcher = described_class.new(env) 19 | 20 | expect(matcher.completions('')).to match_array ['v1.0', 'v1.1'] 21 | end 22 | end 23 | 24 | context 'given a partial tag name' do 25 | it 'returns all tag names matching the input' do 26 | env = double(:env, repo_tags: ['v1.0', 'v2.0']) 27 | matcher = described_class.new(env) 28 | 29 | expect(matcher.completions('v')). 30 | to match_array ['v1.0', 'v2.0'] 31 | expect(matcher.completions('v1')). 32 | to match_array ['v1.0'] 33 | expect(matcher.completions('foo')). 34 | to match_array [] 35 | end 36 | end 37 | end 38 | 39 | describe '#eql?' do 40 | it 'returns true when given another instance of the same class' do 41 | env = double(:env) 42 | matcher1 = described_class.new(env) 43 | matcher2 = described_class.new(env) 44 | 45 | expect(matcher1).to eql(matcher2) 46 | end 47 | 48 | it 'returns false when given an instance of any other class' do 49 | matcher = described_class.new(double(:env)) 50 | other = double(:not_a_matcher) 51 | 52 | expect(matcher).not_to eql(other) 53 | end 54 | end 55 | 56 | describe '#hash' do 57 | it 'returns the same value for all instances of the class' do 58 | env = double(:env) 59 | matcher1 = described_class.new(env) 60 | matcher2 = described_class.new(env) 61 | 62 | expect(matcher1.hash).to eq(matcher2.hash) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/text_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/text_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::TextMatcher do 5 | describe '#match?' do 6 | it 'returns true when given the same word' do 7 | expect(described_class.new('commit').match?('commit')).to be_truthy 8 | end 9 | 10 | it 'returns false when given anything else' do 11 | expect(described_class.new('commit').match?('log')).to be_falsy 12 | end 13 | end 14 | 15 | describe '#completions' do 16 | it 'returns the word, if it begins with the input' do 17 | matcher = described_class.new('commit') 18 | 19 | expect(matcher.completions('foo')).to eq [] 20 | expect(matcher.completions('')).to eq ['commit'] 21 | expect(matcher.completions('commit')).to eq ['commit'] 22 | end 23 | end 24 | 25 | describe '#eql?' do 26 | it 'returns true for text matchers with the same text' do 27 | x1 = described_class.new('x') 28 | x2 = described_class.new('x') 29 | 30 | expect(x1).to eql(x2) 31 | end 32 | 33 | it 'returns false for text matchers with different text' do 34 | x = described_class.new('x') 35 | y = described_class.new('y') 36 | 37 | expect(x).not_to eql(y) 38 | end 39 | 40 | it 'returns false for other types of object' do 41 | x = described_class.new('x') 42 | fake_x = double(:x, word: 'x') 43 | 44 | expect(x).not_to eql(fake_x) 45 | end 46 | end 47 | 48 | describe '#hash' do 49 | it 'returns the same value for instances with the same text' do 50 | x1 = described_class.new('x') 51 | x2 = described_class.new('x') 52 | 53 | expect(x1.hash).to eq(x2.hash) 54 | end 55 | 56 | it 'returns different values for instances with different' do 57 | x = described_class.new('x') 58 | y = described_class.new('y') 59 | 60 | expect(x.hash).not_to eq(y.hash) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/units/tab_completion/matchers/unknown_option_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/matchers/unknown_option_matcher' 3 | 4 | describe Gitsh::TabCompletion::Matchers::UnknownOptionMatcher do 5 | describe '#match?' do 6 | it 'returns true for input starting with "-"' do 7 | matcher = described_class.new 8 | 9 | expect(matcher.match?('-a')).to be_truthy 10 | expect(matcher.match?('--force')).to be_truthy 11 | expect(matcher.match?('--untracked-files')).to be_truthy 12 | end 13 | 14 | it 'returns false for "-" and "--"' do 15 | matcher = described_class.new 16 | 17 | expect(matcher.match?('-')).to be_falsey 18 | expect(matcher.match?('--')).to be_falsey 19 | end 20 | 21 | it 'returns false for input not starting with "-"' do 22 | matcher = described_class.new 23 | 24 | expect(matcher.match?('')).to be_falsey 25 | expect(matcher.match?('push')).to be_falsey 26 | end 27 | end 28 | 29 | describe '#completions' do 30 | it 'returns an empty array for any input' do 31 | matcher = described_class.new 32 | 33 | expect(matcher.completions('')).to eq([]) 34 | expect(matcher.completions('--')).to eq([]) 35 | expect(matcher.completions('--force')).to eq([]) 36 | end 37 | end 38 | 39 | describe '#eql?' do 40 | it 'returns true when given another instance of the same class' do 41 | matcher1 = described_class.new 42 | matcher2 = described_class.new 43 | 44 | expect(matcher1).to eql(matcher2) 45 | end 46 | 47 | it 'returns false when given an instance of any other class' do 48 | matcher = described_class.new 49 | other = double(:not_a_matcher) 50 | 51 | expect(matcher).not_to eql(other) 52 | end 53 | end 54 | 55 | describe '#hash' do 56 | it 'returns the same value for all instances of the class' do 57 | matcher1 = described_class.new 58 | matcher2 = described_class.new 59 | 60 | expect(matcher1.hash).to eq(matcher2.hash) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/units/tab_completion/tokens_to_words_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/tokens_to_words' 3 | 4 | describe Gitsh::TabCompletion::TokensToWords do 5 | describe '#call' do 6 | it 'converts an array of tokens to an array of words' do 7 | expect(call( 8 | [:WORD, 'foo'], [:SPACE], [:WORD, 'bar'], 9 | )).to eq ['foo', 'bar'] 10 | 11 | expect(call( 12 | [:WORD, 'foo'], [:WORD, 'bar'], 13 | )).to eq ['foobar'] 14 | end 15 | 16 | it 'supports variables' do 17 | expect(call( 18 | [:WORD, 'foo'], [:VAR, 'bar'], [:WORD, 'baz'], 19 | )).to eq ['foo${bar}baz'] 20 | end 21 | end 22 | 23 | def call(*token_descriptions) 24 | Gitsh::TabCompletion::TokensToWords.call(tokens(*token_descriptions)) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/units/tab_completion/variable_completer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/variable_completer' 3 | 4 | describe Gitsh::TabCompletion::VariableCompleter do 5 | describe '#call' do 6 | context 'with a variable not wrapped in braces' do 7 | it 'produces variable completions that match the input' do 8 | completer = described_class.new( 9 | build_line_editor, 10 | '$us', 11 | build_env(variables: ['user.name', 'user.email', 'greeting']), 12 | ) 13 | 14 | expect(completer.call).to match_array ['$user.name', '$user.email'] 15 | end 16 | 17 | it 'prefixes the completions with the prefix, if there is one' do 18 | completer = described_class.new( 19 | build_line_editor, 20 | 'name=$us', 21 | build_env(variables: ['user.name', 'user.email', 'greeting']), 22 | ) 23 | 24 | expect(completer.call). 25 | to match_array ['name=$user.name', 'name=$user.email'] 26 | end 27 | 28 | it 'configures the line editor to append a space and not close quotes' do 29 | line_editor = build_line_editor 30 | completer = described_class.new(line_editor, '$us', build_env) 31 | 32 | completer.call 33 | 34 | expect(line_editor). 35 | to have_received(:completion_append_character=).with(nil) 36 | expect(line_editor). 37 | to have_received(:completion_suppress_quote=).with(true) 38 | end 39 | end 40 | 41 | context 'with a variable wrapped in braces' do 42 | it 'produces variable completions that match the input' do 43 | completer = described_class.new( 44 | build_line_editor, 45 | '${us', 46 | build_env(variables: ['user.name', 'user.email', 'greeting']), 47 | ) 48 | 49 | expect(completer.call).to match_array ['${user.name', '${user.email'] 50 | end 51 | 52 | it 'configures the line editor to append a closing brace and not close quotes' do 53 | line_editor = build_line_editor 54 | completer = described_class.new(line_editor, '${us', build_env) 55 | 56 | completer.call 57 | 58 | expect(line_editor). 59 | to have_received(:completion_append_character=).with('}') 60 | expect(line_editor). 61 | to have_received(:completion_suppress_quote=).with(true) 62 | end 63 | end 64 | end 65 | 66 | def build_line_editor 67 | double( 68 | 'LineEditor', 69 | :completion_append_character= => nil, 70 | :completion_suppress_quote= => nil, 71 | ) 72 | end 73 | 74 | def build_env(variables: []) 75 | double( 76 | 'Environment', 77 | available_variables: variables.map(&:to_sym), 78 | ) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/units/tab_completion/visualization_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gitsh/tab_completion/visualization' 3 | 4 | describe Gitsh::TabCompletion::Visualization do 5 | describe '#to_dot' do 6 | it 'produces a description of the state graph in dot format' do 7 | start_state = state('start') 8 | end_state = state('end') 9 | add_text_transition(start_state, 'a', end_state) 10 | automaton = automaton(start_state) 11 | 12 | dot = described_class.new(automaton).to_dot 13 | 14 | expect(dot).to include(%Q{#{start_state.object_id} [ label="start" ]}) 15 | expect(dot).to include(%Q{#{end_state.object_id} [ label="end" ]}) 16 | expect(dot).to include( 17 | %Q{#{start_state.object_id} -> #{end_state.object_id} [ label="\\"a\\"" ]} 18 | ) 19 | end 20 | end 21 | 22 | describe '#summary' do 23 | it 'produces a description of the size of the graph' do 24 | start_state = state('start') 25 | end_state = state('end') 26 | add_text_transition(start_state, 'a', end_state) 27 | end_state.add_free_transition(start_state) 28 | automaton = automaton(start_state) 29 | 30 | summary = described_class.new(automaton).summary 31 | 32 | expect(summary).to include('2 nodes') 33 | expect(summary).to include('2 edges') 34 | end 35 | end 36 | 37 | def automaton(start_state) 38 | Gitsh::TabCompletion::Automaton.new(start_state) 39 | end 40 | 41 | def state(label) 42 | Gitsh::TabCompletion::Automaton::State.new(label) 43 | end 44 | 45 | def add_text_transition(start_state, word, end_state) 46 | start_state.add_transition( 47 | Gitsh::TabCompletion::Matchers::TextMatcher.new(word), 48 | end_state, 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/gitsh.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int 6 | main(int argc, char *argv[]) 7 | { 8 | int ruby_argc; 9 | char **ruby_argv; 10 | 11 | ruby_argc = argc + 2; 12 | ruby_argv = calloc(ruby_argc + 1, sizeof(char *)); 13 | 14 | if (!ruby_argv) { 15 | err(EX_OSERR, GITSH_RB_PATH); 16 | } 17 | 18 | ruby_argv[0] = argv[0]; 19 | ruby_argv[1] = "--disable-gems"; 20 | ruby_argv[2] = GITSH_RB_PATH; 21 | memcpy(&ruby_argv[3], &argv[1], argc * sizeof(char *)); 22 | 23 | ruby_sysinit(&argc, &argv); 24 | ruby_init(); 25 | 26 | return ruby_run_node(ruby_options(ruby_argc, ruby_argv)); 27 | } 28 | -------------------------------------------------------------------------------- /src/gitsh.rb.in: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift('@rubylibdir@') 2 | 3 | require '@gemsetuppath@' 4 | require 'gitsh/environment' 5 | require 'gitsh/cli' 6 | 7 | begin 8 | env = Gitsh::Environment.new(config_directory: '@pkgsysconfdir@') 9 | Gitsh::CLI.new(env: env).run 10 | rescue => e 11 | $stderr.puts "gitsh: Error: #{e.message}" 12 | exit 1 13 | end 14 | -------------------------------------------------------------------------------- /vendor/vendorize: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler' 4 | 5 | unless ARGV.length == 1 6 | $stderr.puts "usage: vendorize path-to-gems" 7 | exit 64 8 | end 9 | 10 | TARGET_PATH = ARGV.first 11 | SETUP_PATH = File.join(TARGET_PATH, 'setup.rb') 12 | 13 | FileUtils.mkdir_p(TARGET_PATH) 14 | 15 | File.open(SETUP_PATH, 'w') do |setup_file| 16 | Bundler.definition.specs_for([:dist]).each do |gem| 17 | if gem.name != 'bundler' 18 | system( 19 | '/usr/bin/env', 'gem', 'unpack', 20 | '--target', TARGET_PATH, 21 | '--version', gem.version.to_s, 22 | gem.name 23 | ) 24 | 25 | template = "$LOAD_PATH.unshift(File.expand_path(%s, __FILE__))" 26 | gem.require_paths.each do |path| 27 | full_path = "../#{gem.name}-#{gem.version}/#{path}" 28 | setup_file.puts(template % full_path.inspect) 29 | end 30 | end 31 | end 32 | end 33 | --------------------------------------------------------------------------------