├── .github └── workflows │ └── multi-ruby-tests.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── Workflow ├── _config.yml ├── bin ├── console └── setup ├── exe └── git-reflow ├── git_reflow.gemspec ├── lib ├── git_reflow.rb └── git_reflow │ ├── base.rb │ ├── config.rb │ ├── git_helpers.rb │ ├── git_server.rb │ ├── git_server │ ├── base.rb │ ├── bit_bucket.rb │ ├── bit_bucket │ │ └── pull_request.rb │ ├── git_hub.rb │ ├── git_hub │ │ └── pull_request.rb │ └── pull_request.rb │ ├── logger.rb │ ├── merge_error.rb │ ├── rspec.rb │ ├── rspec │ ├── command_line_helpers.rb │ ├── stub_helpers.rb │ └── workflow_helpers.rb │ ├── sandbox.rb │ ├── version.rb │ ├── workflow.rb │ └── workflows │ ├── FlatMergeWorkflow │ └── core.rb └── spec ├── fixtures ├── authentication_failure.json ├── awesome_workflow.rb ├── git │ └── git_config ├── issues │ ├── comment.json.erb │ ├── comments.json │ └── comments.json.erb ├── pull_requests │ ├── comment.json.erb │ ├── comments.json │ ├── comments.json.erb │ ├── commits.json │ ├── external_pull_request.json │ ├── pull_request.json │ ├── pull_request.json.erb │ ├── pull_request_branch_nonexistent_error.json │ ├── pull_request_exists_error.json │ ├── pull_requests.json │ ├── review.json.erb │ └── reviews.json.erb ├── repositories │ ├── commit.json │ ├── commit.json.erb │ ├── commits.json.erb │ └── statuses.json └── users │ └── user.json ├── lib ├── git_reflow │ ├── config_spec.rb │ ├── git_helpers_spec.rb │ ├── git_server │ │ ├── bit_bucket_spec.rb │ │ ├── git_hub │ │ │ └── pull_request_spec.rb │ │ ├── git_hub_spec.rb │ │ └── pull_request_spec.rb │ ├── git_server_spec.rb │ ├── logger_spec.rb │ ├── sandbox_spec.rb │ ├── workflow_spec.rb │ └── workflows │ │ ├── core_spec.rb │ │ └── flat_merge_spec.rb └── git_reflow_spec.rb ├── spec_helper.rb └── support ├── fake_github.rb ├── fixtures.rb ├── github_helpers.rb ├── mock_pull_request.rb └── web_mocks.rb /.github/workflows/multi-ruby-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | os: [ ubuntu-latest, macos-latest ] 15 | ruby: ['2.6.9', '2.7.5', '3.0.3 ', '3.1.1'] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true 23 | - name: Run Tests for Ruby ${{ matrix.ruby }} on ${{ matrix.os }} 24 | run: bundle exec rake 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | **/.DS_Store 3 | .rspec 4 | gemfiles 5 | *.gem 6 | .byebug_history 7 | *.log 8 | vendor 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/BlockLength: 2 | Exclude: 3 | - "spec/**/*_spec.rb" 4 | Metrics/LineLength: 5 | Max: 120 6 | Style/ModuleFuction: 7 | EnforcedStyle: extend_self 8 | Style/StringLiterals: 9 | EnforcedStyle: double_quotes 10 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.0 2 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "latest-github-api-release" do 2 | gem "github_api" 3 | end 4 | 5 | appraise "current-reflow-locked-versions" do 6 | gem "github_api", "0.18.2" 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | git_reflow (0.9.9) 5 | bundler (>= 1.10.0) 6 | codenamev_bitbucket_api (= 0.4.1) 7 | colorize (>= 0.8.1) 8 | github_api (= 0.19) 9 | highline 10 | httpclient 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | activesupport (6.1.3) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 1.6, < 2) 18 | minitest (>= 5.1) 19 | tzinfo (~> 2.0) 20 | zeitwerk (~> 2.3) 21 | addressable (2.7.0) 22 | public_suffix (>= 2.0.2, < 5.0) 23 | appraisal (2.4.0) 24 | bundler 25 | rake 26 | thor (>= 0.14.0) 27 | async (1.28.9) 28 | console (~> 1.10) 29 | nio4r (~> 2.3) 30 | timers (~> 4.1) 31 | async-http (0.54.1) 32 | async (~> 1.25) 33 | async-io (~> 1.28) 34 | async-pool (~> 0.2) 35 | protocol-http (~> 0.21.0) 36 | protocol-http1 (~> 0.13.0) 37 | protocol-http2 (~> 0.14.0) 38 | async-http-faraday (0.9.0) 39 | async-http (~> 0.42) 40 | faraday 41 | async-io (1.30.2) 42 | async (~> 1.14) 43 | async-pool (0.3.5) 44 | async (~> 1.25) 45 | byebug (11.1.3) 46 | chronic (0.10.2) 47 | codenamev_bitbucket_api (0.4.1) 48 | faraday (< 2.0) 49 | faraday_middleware (< 2.0) 50 | hashie 51 | multi_json (< 2.0) 52 | nokogiri (>= 1.5.2) 53 | simple_oauth (>= 0.3.0) 54 | coderay (1.1.3) 55 | colorize (0.8.1) 56 | concurrent-ruby (1.1.8) 57 | console (1.10.1) 58 | fiber-local 59 | crack (0.4.5) 60 | rexml 61 | descendants_tracker (0.0.4) 62 | thread_safe (~> 0.3, >= 0.3.1) 63 | diff-lcs (1.4.4) 64 | faraday (1.3.0) 65 | faraday-net_http (~> 1.0) 66 | multipart-post (>= 1.2, < 3) 67 | ruby2_keywords 68 | faraday-http-cache (2.2.0) 69 | faraday (>= 0.8) 70 | faraday-net_http (1.0.1) 71 | faraday_middleware (1.2.0) 72 | faraday (~> 1.0) 73 | fiber-local (1.0.0) 74 | github_api (0.19.0) 75 | addressable (~> 2.4) 76 | descendants_tracker (~> 0.0.4) 77 | faraday (>= 0.8, < 2) 78 | hashie (~> 3.5, >= 3.5.2) 79 | oauth2 (~> 1.0) 80 | github_changelog_generator (1.16.1) 81 | activesupport 82 | async (>= 1.25.0) 83 | async-http-faraday 84 | faraday-http-cache 85 | multi_json 86 | octokit (~> 4.6) 87 | rainbow (>= 2.2.1) 88 | rake (>= 10.0) 89 | retriable (~> 3.0) 90 | hashdiff (1.0.1) 91 | hashie (3.6.0) 92 | highline (2.0.3) 93 | httpclient (2.8.3) 94 | i18n (1.8.9) 95 | concurrent-ruby (~> 1.0) 96 | jwt (2.3.0) 97 | method_source (1.0.0) 98 | mini_portile2 (2.8.0) 99 | minitest (5.14.4) 100 | multi_json (1.15.0) 101 | multi_xml (0.6.0) 102 | multipart-post (2.1.1) 103 | nio4r (2.5.7) 104 | nokogiri (1.13.3) 105 | mini_portile2 (~> 2.8.0) 106 | racc (~> 1.4) 107 | oauth2 (1.4.9) 108 | faraday (>= 0.17.3, < 3.0) 109 | jwt (>= 1.0, < 3.0) 110 | multi_json (~> 1.3) 111 | multi_xml (~> 0.5) 112 | rack (>= 1.2, < 3) 113 | octokit (4.20.0) 114 | faraday (>= 0.9) 115 | sawyer (~> 0.8.0, >= 0.5.3) 116 | protocol-hpack (1.4.2) 117 | protocol-http (0.21.0) 118 | protocol-http1 (0.13.2) 119 | protocol-http (~> 0.19) 120 | protocol-http2 (0.14.2) 121 | protocol-hpack (~> 1.4) 122 | protocol-http (~> 0.18) 123 | pry (0.13.1) 124 | coderay (~> 1.1) 125 | method_source (~> 1.0) 126 | public_suffix (4.0.6) 127 | racc (1.6.0) 128 | rack (2.2.3) 129 | rainbow (3.0.0) 130 | rake (13.0.3) 131 | rdoc (6.3.0) 132 | retriable (3.1.2) 133 | rexml (3.2.4) 134 | rspec (3.10.0) 135 | rspec-core (~> 3.10.0) 136 | rspec-expectations (~> 3.10.0) 137 | rspec-mocks (~> 3.10.0) 138 | rspec-core (3.10.1) 139 | rspec-support (~> 3.10.0) 140 | rspec-expectations (3.10.1) 141 | diff-lcs (>= 1.2.0, < 2.0) 142 | rspec-support (~> 3.10.0) 143 | rspec-mocks (3.10.2) 144 | diff-lcs (>= 1.2.0, < 2.0) 145 | rspec-support (~> 3.10.0) 146 | rspec-support (3.10.2) 147 | ruby2_keywords (0.0.4) 148 | ruby_jard (0.3.1) 149 | byebug (>= 9.1, < 12.0) 150 | pry (~> 0.13.0) 151 | tty-screen (~> 0.8.1) 152 | sawyer (0.8.2) 153 | addressable (>= 2.3.5) 154 | faraday (> 0.8, < 2.0) 155 | simple_oauth (0.3.1) 156 | thor (1.1.0) 157 | thread_safe (0.3.6) 158 | timers (4.3.3) 159 | tty-screen (0.8.1) 160 | tzinfo (2.0.4) 161 | concurrent-ruby (~> 1.0) 162 | webmock (3.12.1) 163 | addressable (>= 2.3.6) 164 | crack (>= 0.3.2) 165 | hashdiff (>= 0.4.0, < 2.0.0) 166 | wwtd (1.4.0) 167 | zeitwerk (2.4.2) 168 | 169 | PLATFORMS 170 | ruby 171 | 172 | DEPENDENCIES 173 | appraisal (= 2.4.0) 174 | chronic 175 | git_reflow! 176 | github_changelog_generator 177 | rake (~> 13.0.3) 178 | rdoc 179 | rspec (~> 3.10) 180 | ruby_jard 181 | webmock 182 | wwtd (= 1.4) 183 | 184 | BUNDLED WITH 185 | 2.2.4 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Reenhanced L.L.C. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | require "github_changelog_generator/task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 9 | config.user = 'reenhanced' 10 | config.project = 'gitreflow' 11 | config.since_tag = 'v0.9.2' 12 | config.future_release = 'master' 13 | end 14 | 15 | task :default => :spec 16 | -------------------------------------------------------------------------------- /Workflow: -------------------------------------------------------------------------------- 1 | # This file is empty on purpose in case there are any custom workflows configured locally. 2 | # Eventually we will update it to: 3 | # use "OpenSourceWorkflow" 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-modernist -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "git_reflow" 5 | 6 | require "irb" 7 | IRB.start 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /exe/git-reflow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib') 3 | require 'rubygems' 4 | require 'git_reflow' 5 | 6 | reflow_command = ARGV.shift 7 | if reflow_command.nil? || GitReflow.workflow.commands[reflow_command.to_sym].nil? 8 | GitReflow.help 9 | elsif ARGV.include? "--help" 10 | GitReflow.documentation_for_command(reflow_command) 11 | else 12 | trap 'INT' do 13 | GitReflow.say "Aborted.", :error 14 | exit 15 | end 16 | 17 | command_options = GitReflow.parse_command_options!(reflow_command) 18 | GitReflow.logger.debug "Running command `#{reflow_command}` with options: #{command_options.inspect}" 19 | GitReflow.public_send(reflow_command.to_sym, command_options) 20 | end 21 | -------------------------------------------------------------------------------- /git_reflow.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Ensure we require the local version and not one we might have installed already 4 | require File.join([File.dirname(__FILE__), 'lib', 'git_reflow/version.rb']) 5 | Gem::Specification.new do |s| 6 | s.name = 'git_reflow' 7 | s.version = GitReflow::VERSION 8 | s.license = 'MIT' 9 | s.authors = ['Valentino Stoll', 'Robert Stern', 'Nicholas Hance'] 10 | s.email = ['dev@reenhanced.com'] 11 | s.homepage = 'http://github.com/reenhanced/gitreflow' 12 | s.summary = 'A better git process' 13 | s.description = 'Git Reflow manages your git workflow.' 14 | s.platform = Gem::Platform::RUBY 15 | s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } 18 | s.bindir = 'exe' 19 | s.require_paths = ['lib'] 20 | s.rdoc_options << '--title' << 'git_reflow' << '-ri' 21 | 22 | s.add_development_dependency('appraisal', '2.4.0') 23 | s.add_development_dependency('chronic') 24 | s.add_development_dependency('github_changelog_generator') 25 | s.add_development_dependency('ruby_jard') 26 | s.add_development_dependency('rake', '~> 13.0.3') 27 | s.add_development_dependency('rdoc') 28 | s.add_development_dependency('rspec', '~> 3.10') 29 | s.add_development_dependency('webmock') 30 | s.add_development_dependency('wwtd', '1.4') 31 | 32 | s.add_dependency('bundler', '>= 1.10.0') 33 | s.add_dependency('codenamev_bitbucket_api', '0.4.1') 34 | s.add_dependency('colorize', '>= 0.8.1') 35 | s.add_dependency('github_api', '0.19') 36 | s.add_dependency('highline') 37 | s.add_dependency('httpclient') 38 | 39 | s.post_install_message = "You need to setup your GitHub OAuth token\nPlease run 'git-reflow setup'" 40 | end 41 | -------------------------------------------------------------------------------- /lib/git_reflow.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'open-uri' 3 | require 'github_api' 4 | require "highline/import" 5 | require 'httpclient' 6 | require 'json' 7 | require 'colorize' 8 | 9 | require 'github_api' 10 | require 'git_reflow/version.rb' unless defined?(GitReflow::VERSION) 11 | require 'git_reflow/config' 12 | require 'git_reflow/git_helpers' 13 | require 'git_reflow/git_server' 14 | require 'git_reflow/git_server/bit_bucket' 15 | require 'git_reflow/git_server/git_hub' 16 | require 'git_reflow/logger' 17 | require 'git_reflow/merge_error' 18 | require 'git_reflow/sandbox' 19 | require 'git_reflow/workflow' 20 | require 'git_reflow/workflows/core' 21 | 22 | # This is a work around to silence logger spam from hashie 23 | # https://github.com/intridea/hashie/issues/394 24 | require "hashie" 25 | require "hashie/logger" 26 | Hashie.logger = Logger.new(nil) 27 | 28 | module GitReflow 29 | include Sandbox 30 | include GitHelpers 31 | 32 | extend self 33 | 34 | def logger(*args) 35 | @logger ||= GitReflow::Logger.new(*args) 36 | end 37 | 38 | def workflow 39 | Workflow.current 40 | end 41 | 42 | def git_server 43 | @git_server ||= GitServer.connect provider: GitReflow::Config.get('reflow.git-server').strip, silent: true 44 | end 45 | 46 | def respond_to_missing?(method_sym, include_all = false) 47 | (workflow && workflow.respond_to?(method_sym, include_all)) || super(method_sym, include_all) 48 | end 49 | 50 | def method_missing(method_sym, *arguments, &block) 51 | if workflow && workflow.respond_to?(method_sym, false) 52 | workflow.send method_sym, *arguments, &block 53 | else 54 | super 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/git_reflow/base.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reenhanced/gitreflow/f23f6a35e3242af18f12531c1656a201f7bad7d0/lib/git_reflow/base.rb -------------------------------------------------------------------------------- /lib/git_reflow/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GitReflow 4 | # This is a utility module for getting and setting git-config variables. 5 | module Config 6 | CONFIG_FILE_PATH = "#{ENV['HOME']}/.gitconfig.reflow" 7 | 8 | module_function 9 | 10 | # Gets the reqested git configuration variable. 11 | # 12 | # @param [String] key The key to get the value(s) for 13 | # @option options [Boolean] :reload (false) whether to reload the value or use a cached value if available 14 | # @option options [Boolean] :all (false) whether to return all keys for a multi-valued key 15 | # @option options [Boolean] :local (false) whether to get the value specific to the current project 16 | # @return the value of the git configuration 17 | def get(key, reload: false, all: false, local: false, **_other_options) 18 | return cached_git_config_value(key) unless reload || cached_git_config_value(key).empty? 19 | 20 | local = local ? '--local ' : '' 21 | if all 22 | new_value = GitReflow::Sandbox.run("git config #{local}--get-all #{key}", loud: false, blocking: false) 23 | else 24 | new_value = GitReflow::Sandbox.run("git config #{local}--get #{key}", loud: false, blocking: false) 25 | end 26 | cache_git_config_key(key, new_value) 27 | end 28 | 29 | # Sets the reqested git configuration variable. 30 | # 31 | # @param [String] key The key to set the value for 32 | # @param [String] value The value to set it to 33 | # @option options [Boolean] :local (false) whether to set the value specific to the current project 34 | # @return the value of the git configuration 35 | def set(key, value, local: false, **_other_options) 36 | value = value.to_s.strip 37 | if local 38 | GitReflow::Sandbox.run "git config --replace-all #{key} \"#{value}\"", loud: false, blocking: false 39 | else 40 | GitReflow::Sandbox.run "git config -f #{CONFIG_FILE_PATH} --replace-all #{key} \"#{value}\"", loud: false, blocking: false 41 | end 42 | end 43 | 44 | # Remove values of the reqested git configuration variable. 45 | # 46 | # @param [String] key The key to remove 47 | # @option options [Boolean] :value (nil) The value of the key to remove 48 | # @option options [Boolean] :local (false) whether to remove the value specific to the current project 49 | # @return the result of running the git command 50 | def unset(key, value: nil, local: false, **_other_options) 51 | value = value.nil? ? '' : "\"#{value}\"" 52 | if local 53 | GitReflow::Sandbox.run "git config --unset-all #{key} #{value}", loud: false, blocking: false 54 | else 55 | GitReflow::Sandbox.run "git config -f #{CONFIG_FILE_PATH} --unset-all #{key} #{value}", loud: false, blocking: false 56 | end 57 | end 58 | 59 | # Adds a new git configuration variable. 60 | # 61 | # @param [String] key The new key to set the value for 62 | # @param [String] value The value to set it to 63 | # @option options [Boolean] :local (false) whether to set the value specific to the current project 64 | # @option options [Boolean] :global (false) whether to set the value globaly. if neither local or global is set gitreflow will default to using a configuration file 65 | # @return the result of running the git command 66 | def add(key, value, local: false, global: false, **_other_options) 67 | if global 68 | GitReflow::Sandbox.run "git config --global --add #{key} \"#{value}\"", loud: false, blocking: false 69 | elsif local 70 | GitReflow::Sandbox.run "git config --add #{key} \"#{value}\"", loud: false, blocking: false 71 | else 72 | GitReflow::Sandbox.run "git config -f #{CONFIG_FILE_PATH} --add #{key} \"#{value}\"", loud: false, blocking: false 73 | end 74 | end 75 | 76 | def cached_git_config_value(key) 77 | instance_variable_get(:"@#{key.tr('.-', '_')}").to_s 78 | end 79 | 80 | def cache_git_config_key(key, value) 81 | instance_variable_set(:"@#{key.tr('.-', '_')}", value.to_s.strip) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/git_reflow/git_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'git_reflow/config' 4 | require 'git_reflow/sandbox' 5 | 6 | module GitReflow 7 | # Includes many helper methods for common tasks within a git repository. 8 | module GitHelpers 9 | include Sandbox 10 | 11 | def default_editor 12 | ENV['EDITOR'] || 'vi' 13 | end 14 | 15 | def git_root_dir 16 | return @git_root_dir unless @git_root_dir.to_s.empty? 17 | return @git_root_dir = Dir.pwd if Dir.exists?("#{Dir.pwd}/.git") 18 | 19 | @git_root_dir = run('git rev-parse --show-toplevel', loud: false).strip 20 | end 21 | 22 | def git_editor_command 23 | git_editor = GitReflow::Config.get('core.editor') 24 | if !git_editor.empty? 25 | git_editor 26 | else 27 | default_editor 28 | end 29 | end 30 | 31 | def remote_user 32 | return '' if GitReflow::Config.get('remote.origin.url').empty? 33 | extract_remote_user_and_repo_from_remote_url(GitReflow::Config.get('remote.origin.url'))[:user] 34 | end 35 | 36 | def remote_repo_name 37 | return '' if GitReflow::Config.get('remote.origin.url').empty? 38 | extract_remote_user_and_repo_from_remote_url(GitReflow::Config.get('remote.origin.url'))[:repo] 39 | end 40 | 41 | def default_base_branch 42 | base_branch_name = GitReflow::Config.get('reflow.base-branch') 43 | return 'master' if base_branch_name.empty? 44 | base_branch_name 45 | end 46 | 47 | def current_branch 48 | run("git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g'", loud: false).strip 49 | end 50 | 51 | def pull_request_template 52 | custom_template = GitReflow::Config.get('templates.pull-request') 53 | filenames_to_try = %w[ 54 | .github/PULL_REQUEST_TEMPLATE.md 55 | .github/PULL_REQUEST_TEMPLATE 56 | PULL_REQUEST_TEMPLATE.md 57 | PULL_REQUEST_TEMPLATE 58 | ].map do |file| 59 | "#{git_root_dir}/#{file}" 60 | end 61 | filenames_to_try.unshift(custom_template) unless custom_template.empty? 62 | 63 | parse_first_matching_template_file(filenames_to_try) 64 | end 65 | 66 | def merge_commit_template 67 | custom_template = GitReflow::Config.get('templates.merge-commit') 68 | filenames_to_try = %w[.github/MERGE_COMMIT_TEMPLATE.md 69 | .github/MERGE_COMMIT_TEMPLATE 70 | MERGE_COMMIT_TEMPLATE.md 71 | MERGE_COMMIT_TEMPLATE].map do |file| 72 | "#{git_root_dir}/#{file}" 73 | end 74 | filenames_to_try.unshift(custom_template) unless custom_template.empty? 75 | 76 | parse_first_matching_template_file(filenames_to_try) 77 | end 78 | 79 | def get_first_commit_message 80 | run('git log --pretty=format:"%s" --no-merges -n 1', loud: false).strip 81 | end 82 | 83 | def push_current_branch(options = {}) 84 | remote = options[:remote] || 'origin' 85 | run_command_with_label "git push #{remote} #{current_branch}" 86 | end 87 | 88 | def update_current_branch(options = {}) 89 | remote = options[:remote] || 'origin' 90 | run_command_with_label "git pull #{remote} #{current_branch}" 91 | push_current_branch(options) 92 | end 93 | 94 | def fetch_destination(destination_branch) 95 | run_command_with_label "git fetch origin #{destination_branch}" 96 | end 97 | 98 | def update_destination(destination_branch, options = {}) 99 | origin_branch = current_branch 100 | remote = options[:remote] || 'origin' 101 | run_command_with_label "git checkout #{destination_branch}" 102 | run_command_with_label "git pull #{remote} #{destination_branch}" 103 | run_command_with_label "git checkout #{origin_branch}" 104 | end 105 | 106 | def update_feature_branch(options = {}) 107 | base_branch = options[:base] 108 | remote = options[:remote] 109 | update_destination(base_branch, options) 110 | 111 | # update feature branch in case there are multiple authors and remote changes 112 | run_command_with_label "git pull origin #{current_branch}" 113 | # rebase on base branch 114 | run_command_with_label "git merge #{base_branch}" 115 | end 116 | 117 | def append_to_merge_commit_message(message = '', merge_method: "squash") 118 | tmp_merge_message_path = "#{git_root_dir}/.git/tmp_merge_msg" 119 | dest_merge_message_path = merge_message_path(merge_method: merge_method) 120 | 121 | run "touch #{tmp_merge_message_path}" 122 | 123 | File.open(tmp_merge_message_path, "w") do |file_content| 124 | file_content.puts message 125 | if File.exists? dest_merge_message_path 126 | File.foreach(dest_merge_message_path) do |line| 127 | file_content.puts line 128 | end 129 | end 130 | end 131 | 132 | run "mv #{tmp_merge_message_path} #{dest_merge_message_path}" 133 | end 134 | 135 | def merge_message_path(merge_method: nil) 136 | merge_method = merge_method || GitReflow::Config.get("reflow.merge-method") 137 | merge_method = "squash" if "#{merge_method}".length < 1 138 | if merge_method =~ /squash/i 139 | "#{git_root_dir}/.git/SQUASH_MSG" 140 | else 141 | "#{git_root_dir}/.git/MERGE_MSG" 142 | end 143 | end 144 | 145 | private 146 | 147 | def parse_first_matching_template_file(template_file_names) 148 | filename = template_file_names.detect do |file| 149 | File.exist? file 150 | end 151 | 152 | # Thanks to @Shalmezad for contribuiting the template `gsub` snippet :-) 153 | # https://github.com/reenhanced/gitreflow/issues/51#issuecomment-253535093 154 | if filename 155 | template_content = File.read filename 156 | template_content.gsub!(/\{\{([a-zA-Z_]+[a-zA-Z0-9_]*)\}\}/) { GitReflow.public_send($1) } 157 | template_content 158 | end 159 | end 160 | 161 | def extract_remote_user_and_repo_from_remote_url(remote_url) 162 | result = { user: '', repo: '' } 163 | return result unless "#{remote_url}".length > 0 164 | 165 | if remote_url =~ /\Agit@/i 166 | result[:user] = remote_url[/[\/:](\w|-|\.)+/i][1..-1] 167 | result[:repo] = remote_url[/\/(\w|-|\.)+$/i][1..-5] 168 | elsif remote_url =~ /\Ahttps?/i 169 | result[:user] = remote_url.split('/')[-2] 170 | result[:repo] = remote_url.split('/')[-1].gsub(/.git\Z/i, '') 171 | end 172 | 173 | result 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module GitServer 3 | autoload :Base, 'git_reflow/git_server/base' 4 | autoload :GitHub, 'git_reflow/git_server/git_hub' 5 | autoload :PullRequest, 'git_reflow/git_server/pull_request' 6 | 7 | extend self 8 | 9 | class ConnectionError < StandardError; end 10 | 11 | def connect(options = {}) 12 | options ||= {} 13 | options[:provider] = 'GitHub' if "#{options[:provider]}".length <= 0 14 | begin 15 | provider_name = options[:provider] 16 | provider = provider_class_for(options.delete(:provider)).new(options) 17 | provider.authenticate(options.keep_if {|key, value| key == :silent }) 18 | provider 19 | rescue ConnectionError => e 20 | GitReflow.say "Error connecting to #{provider_name}: #{e.message}", :error 21 | end 22 | end 23 | 24 | def connection 25 | return nil unless current_provider 26 | current_provider.connection 27 | end 28 | 29 | def current_provider 30 | provider = "#{GitReflow::Config.get('reflow.git-server', local: true) || GitReflow::Config.get('reflow.git-server')}" 31 | if provider.length > 0 32 | begin 33 | provider_class_for(provider) 34 | rescue ConnectionError => e 35 | GitReflow.say e.message, :error 36 | nil 37 | end 38 | else 39 | GitReflow.say "Reflow hasn't been setup yet. Run 'git reflow setup' to continue", :notice 40 | nil 41 | end 42 | end 43 | 44 | def can_connect_to?(provider) 45 | GitReflow::GitServer.const_defined?(provider) 46 | end 47 | 48 | def create_pull_request(options = {}) 49 | raise "#{self.class.to_s}#create_pull_request method must be implemented" 50 | end 51 | 52 | def find_open_pull_request(options = {}) 53 | raise "#{self.class.to_s}#find_open_pull_request method must be implemented" 54 | end 55 | 56 | private 57 | 58 | def provider_class_for(provider) 59 | raise ConnectionError, "GitServer not setup for \"#{provider}\"" unless self.can_connect_to?(provider) 60 | GitReflow::GitServer.const_get(provider) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/base.rb: -------------------------------------------------------------------------------- 1 | require 'git_reflow/config' 2 | 3 | module GitReflow 4 | class GitServer::Base 5 | extend GitHelpers 6 | 7 | @@connection = nil 8 | 9 | def initialize(options) 10 | site_url = self.class.site_url 11 | api_endpoint = self.class.api_endpoint 12 | 13 | self.class.site_url = site_url 14 | self.class.api_endpoint = api_endpoint 15 | 16 | authenticate 17 | end 18 | 19 | def self.connection 20 | raise "#{self.class.to_s}.connection method must be implemented" 21 | end 22 | 23 | def self.user 24 | raise "#{self.class.to_s}.user method must be implemented" 25 | end 26 | 27 | def self.api_endpoint 28 | raise "#{self.class.to_s}.api_endpoint method must be implemented" 29 | end 30 | 31 | def self.api_endpoint=(api_endpoint, options = {local: false}) 32 | raise "#{self.class.to_s}.api_endpoint= method must be implemented" 33 | end 34 | 35 | def self.site_url 36 | raise "#{self.class.to_s}.site_url method must be implemented" 37 | end 38 | 39 | def self.site_url=(site_url, options = {local: false}) 40 | raise "#{self.class.to_s}.site_url= method must be implemented" 41 | end 42 | 43 | def self.project_only? 44 | GitReflow::Config.get("reflow.local-projects", all: true).include? "#{remote_user}/#{remote_repo_name}" 45 | end 46 | 47 | def connection 48 | @connection ||= self.class.connection 49 | end 50 | 51 | def authenticate 52 | raise "#{self.class.to_s}#authenticate method must be implemented" 53 | end 54 | 55 | def find_open_pull_request(options) 56 | raise "#{self.class.to_s}#find_open_pull_request(options) method must be implemented" 57 | end 58 | 59 | def get_build_status sha 60 | raise "#{self.class.to_s}#get_build_status(sha) method must be implemented" 61 | end 62 | 63 | def colorized_build_description status 64 | raise "#{self.class.to_s}#colorized_build_description(status) method must be implemented" 65 | end 66 | 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/bit_bucket.rb: -------------------------------------------------------------------------------- 1 | require 'bitbucket_rest_api' 2 | require 'git_reflow/git_helpers' 3 | 4 | module GitReflow 5 | module GitServer 6 | class BitBucket < Base 7 | require_relative 'bit_bucket/pull_request' 8 | 9 | attr_reader :connection 10 | 11 | def initialize(config_options = {}) 12 | project_only = !!config_options.delete(:project_only) 13 | 14 | # We remove any existing setup first, then setup our required config settings 15 | GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}") 16 | GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}") if project_only 17 | GitReflow::Config.set('reflow.git-server', 'BitBucket', local: project_only) 18 | end 19 | 20 | def self.connection 21 | if api_key_setup? 22 | @connection ||= ::BitBucket.new login: remote_user, password: api_key 23 | end 24 | end 25 | 26 | def self.api_endpoint 27 | endpoint = GitReflow::Config.get("bitbucket.endpoint", local: project_only?) 28 | (endpoint.length > 0) ? endpoint : ::BitBucket::Configuration::DEFAULT_ENDPOINT 29 | end 30 | 31 | def self.site_url 32 | site_url = GitReflow::Config.get("bitbucket.site", local: project_only?) 33 | (site_url.length > 0) ? site_url : 'https://bitbucket.org' 34 | end 35 | 36 | def self.api_key 37 | GitReflow::Config.get("bitbucket.api-key", reload: true, local: project_only?) 38 | end 39 | 40 | def self.api_key=(key) 41 | GitReflow::Config.set("bitbucket.api-key", key, local: project_only?) 42 | end 43 | def self.api_key_setup? 44 | (self.api_key.length > 0) 45 | end 46 | 47 | def self.user 48 | GitReflow::Config.get('bitbucket.user', local: project_only?) 49 | end 50 | 51 | def self.user=(bitbucket_user) 52 | GitReflow::Config.set('bitbucket.user', bitbucket_user, local: project_only?) 53 | end 54 | 55 | def authenticate(options = {silent: false}) 56 | begin 57 | if connection and self.class.api_key_setup? 58 | unless options[:silent] 59 | GitReflow.say "\nYour BitBucket account was already setup with:" 60 | GitReflow.say "\tUser Name: #{self.class.user}" 61 | end 62 | else 63 | self.class.user = options[:user] || ask("Please enter your BitBucket username: ") 64 | GitReflow.say "\nIn order to connect your BitBucket account," 65 | GitReflow.say "you'll need to generate an API key for your team" 66 | GitReflow.say "Visit #{self.class.site_url}/account/user/#{self.class.remote_user}/api-key/, to generate it\n" 67 | self.class.api_key = ask("Please enter your team's API key: ") 68 | connection.repos.all(self.class.remote_user).count 69 | GitReflow.say "Connected to BitBucket\!", :success 70 | end 71 | rescue ::BitBucket::Error::Unauthorized => e 72 | GitReflow::Config.unset('bitbucket.api-key', local: self.class.project_only?) 73 | GitReflow.say "Invalid API key for team #{self.class.remote_user}.", :error 74 | end 75 | end 76 | 77 | def connection 78 | @connection ||= self.class.connection 79 | end 80 | 81 | def get_build_status(sha) 82 | # BitBucket does not currently support build status via API 83 | # for updates: https://bitbucket.org/site/master/issue/8548/better-ci-integration-add-a-build-status 84 | return nil 85 | end 86 | 87 | def colorized_build_description(state, description) 88 | "" 89 | end 90 | 91 | def create_pull_request(options = {}) 92 | PullRequest.create(options) 93 | end 94 | 95 | def find_open_pull_request(options = {}) 96 | PullRequest.find_open(options) 97 | end 98 | 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/bit_bucket/pull_request.rb: -------------------------------------------------------------------------------- 1 | require 'git_reflow/git_server/pull_request' 2 | 3 | module GitReflow 4 | module GitServer 5 | class BitBucket 6 | class PullRequest < GitReflow::GitServer::PullRequest 7 | def initialize(attributes) 8 | self.number = attributes.id 9 | self.description = attributes.body || attributes.description 10 | self.html_url = "#{attributes.source.repository.links.html.href}/pull-request/#{self.number}" 11 | self.feature_branch_name = attributes.source.branch.name[/[^:]+$/] 12 | self.base_branch_name = attributes.destination.branch.name[/[^:]+$/] 13 | self.build = Build.new 14 | self.source_object = attributes 15 | end 16 | 17 | def self.create(options = {}) 18 | self.new GitReflow.git_server.connection.repos.pull_requests.create( 19 | GitReflow.git_server.class.remote_user, 20 | GitReflow.git_server.class.remote_repo_name, 21 | title: options[:title], 22 | description: options[:body] || options[:description], 23 | source: { 24 | branch: { name: GitReflow.git_server.class.current_branch }, 25 | repository: { full_name: "#{GitReflow.git_server.class.remote_user}/#{GitReflow.git_server.class.remote_repo_name}" } 26 | }, 27 | destination: { 28 | branch: { name: options[:base] } 29 | }, 30 | reviewers: [username: GitReflow.git_server.class.user]) 31 | end 32 | 33 | def self.find_open(to: 'master', from: GitReflow.git_server.class.current_branch) 34 | begin 35 | matching_pull = GitReflow.git_server.connection.repos.pull_requests.all(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, limit: 1).select do |pr| 36 | pr.source.branch.name == from and 37 | pr.destination.branch.name == to 38 | end.first 39 | 40 | if matching_pull 41 | self.new matching_pull 42 | end 43 | rescue ::BitBucket::Error::NotFound => e 44 | GitReflow.git_server.say "No BitBucket repo found for #{GitReflow.git_server.class.remote_user}/#{GitReflow.git_server.class.remote_repo_name}", :error 45 | rescue ::BitBucket::Error::Forbidden => e 46 | GitReflow.git_server.say "You don't have API access to this repo", :error 47 | end 48 | end 49 | 50 | def commit_author 51 | # use the author of the pull request 52 | self.author.username 53 | end 54 | 55 | def comments 56 | GitReflow.git_server.connection.repos.pull_requests.comments.all(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, self.id) 57 | end 58 | 59 | def last_comment 60 | last_comment = comments.first 61 | return "" unless last_comment 62 | "#{last_comment.content.raw}" 63 | end 64 | 65 | def reviewers 66 | return [] unless comments.size > 0 67 | comments.map {|c| c.user.username }.uniq - [GitReflow.git_server.class.user] 68 | end 69 | 70 | def approvals 71 | approved = [] 72 | 73 | GitReflow.git_server.connection.repos.pull_requests.activity(GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, self.id).each do |activity| 74 | break unless activity.respond_to?(:approval) and activity.approval.user.username != GitReflow.git_server.class.user 75 | approved |= [activity.approval.user.username] 76 | end 77 | 78 | approved 79 | end 80 | 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/git_hub.rb: -------------------------------------------------------------------------------- 1 | require 'github_api' 2 | require 'git_reflow/git_helpers' 3 | 4 | module GitReflow 5 | module GitServer 6 | class GitHub < Base 7 | require_relative 'git_hub/pull_request' 8 | 9 | extend GitHelpers 10 | include Sandbox 11 | 12 | attr_reader :connection 13 | 14 | def initialize(config_options = {}) 15 | project_only = !!config_options.delete(:project_only) 16 | using_enterprise = !!config_options.delete(:enterprise) 17 | 18 | gh_site_url = self.class.site_url 19 | gh_api_endpoint = self.class.api_endpoint 20 | 21 | if using_enterprise 22 | gh_site_url = ask("Please enter your Enterprise site URL (e.g. https://github.company.com):") 23 | gh_api_endpoint = ask("Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):") 24 | end 25 | 26 | self.class.site_url = gh_site_url 27 | self.class.api_endpoint = gh_api_endpoint 28 | 29 | # We remove any existing setup first, then setup our required config settings 30 | GitReflow::Config.unset('reflow.local-projects', value: "#{self.class.remote_user}/#{self.class.remote_repo_name}") 31 | GitReflow::Config.add('reflow.local-projects', "#{self.class.remote_user}/#{self.class.remote_repo_name}") if project_only 32 | GitReflow::Config.set('reflow.git-server', 'GitHub', local: project_only) 33 | end 34 | 35 | def self.connection 36 | if self.oauth_token.length > 0 37 | @connection ||= ::Github.new do |config| 38 | config.oauth_token = GitServer::GitHub.oauth_token 39 | config.endpoint = GitServer::GitHub.api_endpoint 40 | config.site = GitServer::GitHub.site_url 41 | end 42 | end 43 | end 44 | 45 | def self.user 46 | GitReflow::Config.get('github.user') 47 | end 48 | 49 | def self.user=(github_user) 50 | GitReflow::Config.set('github.user', github_user, local: project_only?) 51 | end 52 | 53 | def self.oauth_token 54 | GitReflow::Config.get('github.oauth-token') 55 | end 56 | 57 | def self.oauth_token=(oauth_token) 58 | GitReflow::Config.set('github.oauth-token', oauth_token, local: project_only?) 59 | oauth_token 60 | end 61 | 62 | def self.api_endpoint 63 | endpoint = "#{GitReflow::Config.get('github.endpoint')}".strip 64 | (endpoint.length > 0) ? endpoint : ::Github.endpoint 65 | end 66 | 67 | def self.api_endpoint=(api_endpoint) 68 | GitReflow::Config.set("github.endpoint", api_endpoint, local: project_only?) 69 | api_endpoint 70 | end 71 | 72 | def self.site_url 73 | site_url = "#{GitReflow::Config.get('github.site')}".strip 74 | (site_url.length > 0) ? site_url : ::Github.site 75 | end 76 | 77 | def self.site_url=(site_url) 78 | GitReflow::Config.set("github.site", site_url, local: project_only?) 79 | site_url 80 | end 81 | 82 | def connection 83 | @connection ||= self.class.connection 84 | end 85 | 86 | def authenticate(options = {silent: false}) 87 | if !options[:user].to_s.empty? 88 | self.class.user = options[:user] 89 | elsif self.class.user.empty? 90 | self.class.user = ask("Please enter your GitHub username: ") 91 | end 92 | 93 | if connection and self.class.oauth_token.length > 0 94 | begin 95 | connection.users.get 96 | unless options[:silent] 97 | GitReflow.say "Your GitHub account was already setup with: " 98 | GitReflow.say "\tUser Name: #{self.class.user}" 99 | GitReflow.say "\tEndpoint: #{self.class.api_endpoint}" 100 | end 101 | return connection 102 | rescue ::Github::Error::Unauthorized => e 103 | GitReflow.logger.debug "[GitHub Error] Current oauth-token is invalid or expired..." 104 | end 105 | end 106 | 107 | begin 108 | gh_password = options[:password] || ask("Please enter your GitHub password (we do NOT store this): ") { |q| q.echo = false } 109 | 110 | @connection = ::Github.new do |config| 111 | config.basic_auth = "#{self.class.user}:#{gh_password}" 112 | config.endpoint = GitServer::GitHub.api_endpoint 113 | config.site = GitServer::GitHub.site_url 114 | config.adapter = :net_http 115 | end 116 | 117 | @connection.connection_options = {headers: {"X-GitHub-OTP" => options[:two_factor_auth_code]}} if options[:two_factor_auth_code] 118 | 119 | previous_authorizations = @connection.oauth.all.select {|auth| auth.note == "git-reflow (#{run('hostname', loud: false).strip})" } 120 | if previous_authorizations.any? 121 | authorization = previous_authorizations.last 122 | GitReflow.say "You have previously setup git-reflow on this machine, but we can no longer find the stored token.", :error 123 | GitReflow.say "Please visit https://github.com/settings/tokens and delete the token for: git-reflow (#{run('hostname', loud: false).strip})", :notice 124 | raise "Setup could not be completed." 125 | else 126 | authorization = @connection.oauth.create scopes: ['repo'], note: "git-reflow (#{run('hostname', loud: false).strip})" 127 | end 128 | 129 | self.class.oauth_token = authorization.token 130 | 131 | rescue ::Github::Error::Unauthorized => e 132 | if e.inspect.to_s.include?('two-factor') 133 | begin 134 | # dummy request to trigger a 2FA SMS since a HTTP GET won't do it 135 | @connection.oauth.create scopes: ['repo'], note: "thank Github for not making this straightforward" 136 | rescue ::Github::Error::Unauthorized 137 | ensure 138 | two_factor_code = ask("Please enter your two-factor authentication code: ") 139 | self.authenticate options.merge({user: self.class.user, password: gh_password, two_factor_auth_code: two_factor_code}) 140 | end 141 | else 142 | GitReflow.say "Github Authentication Error: #{e.inspect}", :error 143 | raise "Setup could not be completed." 144 | end 145 | rescue StandardError => e 146 | raise "We were unable to authenticate with Github." 147 | else 148 | GitReflow.say "Your GitHub account was successfully setup!", :success 149 | 150 | end 151 | 152 | @connection 153 | end 154 | 155 | def get_build_status(sha) 156 | connection.repos.statuses.all(self.class.remote_user, self.class.remote_repo_name, sha).first 157 | end 158 | 159 | def colorized_build_description(state, description) 160 | colorized_statuses = { 161 | pending: :yellow, 162 | success: :green, 163 | error: :red, 164 | failure: :red } 165 | description.colorize( colorized_statuses[state.to_sym] ) 166 | end 167 | 168 | def create_pull_request(options = {}) 169 | PullRequest.create(options) 170 | end 171 | 172 | def find_open_pull_request(options = {}) 173 | PullRequest.find_open(options) 174 | end 175 | 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/git_hub/pull_request.rb: -------------------------------------------------------------------------------- 1 | require 'git_reflow/git_server/pull_request' 2 | 3 | module GitReflow 4 | module GitServer 5 | class GitHub 6 | class PullRequest < GitReflow::GitServer::PullRequest 7 | def initialize(attributes) 8 | self.number = attributes.number 9 | self.description = attributes[:body] 10 | self.html_url = attributes.html_url 11 | self.feature_branch_name = attributes.head.label[/[^:]+$/] 12 | self.base_branch_name = attributes.base.label[/[^:]+$/] 13 | self.source_object = attributes 14 | self.build = Build.new(state: build.state, description: build.description, url: build.url) 15 | end 16 | 17 | def self.create(options = {}) 18 | self.new(GitReflow.git_server.connection.pull_requests.create( 19 | GitReflow.git_server.class.remote_user, 20 | GitReflow.git_server.class.remote_repo_name, 21 | title: options[:title], 22 | body: options[:body], 23 | head: "#{GitReflow.git_server.class.remote_user}:#{GitReflow.git_server.class.current_branch}", 24 | base: options[:base])) 25 | end 26 | 27 | def self.find_open(options = {}) 28 | options[:to] ||= 'master' 29 | options[:from] ||= GitReflow.git_server.class.current_branch 30 | 31 | matching_pull = GitReflow.git_server.connection.pull_requests.all( 32 | GitReflow.remote_user, 33 | GitReflow.remote_repo_name, 34 | base: options[:to], 35 | head: "#{GitReflow.remote_user}:#{options[:from]}", 36 | state: 'open' 37 | ).first 38 | 39 | if matching_pull 40 | self.new matching_pull 41 | end 42 | end 43 | 44 | # override attr_reader for auto-updates 45 | def build_status 46 | @build_status ||= build.state 47 | end 48 | 49 | def commit_author 50 | begin 51 | username, _ = base.label.split(':') 52 | first_commit = GitReflow.git_server.connection.pull_requests.commits(username, GitReflow.git_server.class.remote_repo_name, number.to_s).first 53 | "#{first_commit.commit.author.name} <#{first_commit.commit.author.email}>".strip 54 | rescue Github::Error::NotFound 55 | nil 56 | end 57 | end 58 | 59 | def reviewers 60 | (comment_authors + pull_request_reviews.map(&:user).map(&:login)).uniq - [user.login] 61 | end 62 | 63 | def approvals 64 | pull_last_committed_at = get_committed_time(self.head.sha) 65 | 66 | approved_reviewers = pull_request_reviews.select { |r| r.state == 'APPROVED' }.map(&:user).map(&:login) 67 | 68 | ( 69 | comment_authors(with: self.class.approval_regex, after: pull_last_committed_at) + 70 | approved_reviewers 71 | ).uniq 72 | end 73 | 74 | def comments 75 | comments = GitReflow.git_server.connection.issues.comments.all GitReflow.remote_user, GitReflow.remote_repo_name, number: self.number 76 | review_comments = GitReflow.git_server.connection.pull_requests.comments.all GitReflow.remote_user, GitReflow.remote_repo_name, number: self.number 77 | 78 | (review_comments.to_a + comments.to_a).select { |c| c.user.login != user.login } 79 | end 80 | 81 | def last_comment 82 | if comments.last.nil? 83 | "" 84 | else 85 | "#{comments.last.body.inspect}" 86 | end 87 | end 88 | 89 | def approved? 90 | if self.class.minimum_approvals.to_i == 0 91 | super 92 | else 93 | approvals.size >= self.class.minimum_approvals.to_i and ( 94 | last_comment.empty? || 95 | !last_comment.match(self.class.approval_regex).nil? 96 | ) 97 | end 98 | end 99 | 100 | def merge!(options = {}) 101 | 102 | # fallback to default merge process if user "forces" merge 103 | if(options[:force]) 104 | super options 105 | else 106 | if deliver? 107 | GitReflow.say "Merging pull request ##{self.number}: '#{self.title}', from '#{self.feature_branch_name}' into '#{self.base_branch_name}'", :notice 108 | 109 | merge_method = options[:merge_method] || GitReflow::Config.get("reflow.merge-method") 110 | merge_method = "squash" if "#{merge_method}".length < 1 111 | merge_message_file = GitReflow.merge_message_path(merge_method: merge_method) 112 | 113 | unless options[:title] || options[:message] 114 | # prompts user for commit_title and commit_message 115 | File.open(merge_message_file, 'w') do |file| 116 | file.write("#{self.title}\n#{self.commit_message_for_merge}\n") 117 | end 118 | 119 | GitReflow.run("#{GitReflow.git_editor_command} #{merge_message_file}", with_system: true) 120 | merge_message = File.read(merge_message_file).split(/[\r\n]|\r\n/).map(&:strip) 121 | 122 | title = merge_message.shift 123 | 124 | File.delete(merge_message_file) 125 | 126 | unless merge_message.empty? 127 | merge_message.shift if merge_message.first.empty? 128 | end 129 | 130 | options[:title] = title 131 | options[:body] = "#{merge_message.join("\n")}\n" 132 | 133 | GitReflow.say "\nReview your merge commit message:\n" 134 | GitReflow.say "--------\n" 135 | GitReflow.say "Title:\n#{options[:title]}\n\n" 136 | GitReflow.say "Body:\n#{options[:body]}\n" 137 | GitReflow.say "--------\n" 138 | end 139 | 140 | options[:body] = "#{options[:message]}\n" if options[:body].nil? and "#{options[:message]}".length > 0 141 | 142 | merge_response = GitReflow::GitServer::GitHub.connection.pull_requests.merge( 143 | "#{GitReflow.git_server.class.remote_user}", 144 | "#{GitReflow.git_server.class.remote_repo_name}", 145 | "#{self.number}", 146 | { 147 | "commit_title" => "#{options[:title]}", 148 | "commit_message" => "#{options[:body]}", 149 | "sha" => "#{self.head.sha}", 150 | "merge_method" => merge_method 151 | } 152 | ) 153 | 154 | if merge_response.success? 155 | GitReflow.run_command_with_label "git checkout #{self.base_branch_name}" 156 | # Pulls merged changes from remote base_branch 157 | GitReflow.run_command_with_label "git pull origin #{self.base_branch_name}" 158 | GitReflow.say "Pull request ##{self.number} successfully merged.", :success 159 | 160 | if cleanup_remote_feature_branch? 161 | GitReflow.run_command_with_label "git push origin :#{self.feature_branch_name}", blocking: false 162 | else 163 | GitReflow.say "Skipped. Remote feature branch #{self.feature_branch_name} left in tact." 164 | end 165 | 166 | if cleanup_local_feature_branch? 167 | GitReflow.run_command_with_label "git branch -D #{self.feature_branch_name}" 168 | else 169 | GitReflow.say "Skipped. Local feature branch #{self.feature_branch_name} left in tact." 170 | end 171 | 172 | GitReflow.say "Nice job buddy." 173 | else 174 | GitReflow.say merge_response.to_s, :deliver_halted 175 | GitReflow.say "There were problems commiting your feature... please check the errors above and try again.", :error 176 | end 177 | else 178 | GitReflow.say "Merge aborted", :deliver_halted 179 | end 180 | end 181 | end 182 | 183 | def build 184 | github_build_status = GitReflow.git_server.get_build_status(self.head.sha) 185 | if github_build_status 186 | Build.new( 187 | state: github_build_status.state, 188 | description: github_build_status.description, 189 | url: github_build_status.target_url 190 | ) 191 | else 192 | Build.new 193 | end 194 | end 195 | 196 | private 197 | 198 | def pull_request_reviews 199 | @pull_request_reviews ||= GitReflow.git_server.connection.pull_requests.reviews.list( 200 | user: base.label.split(':').first, 201 | repo: GitReflow.git_server.class.remote_repo_name, number: number 202 | ) 203 | end 204 | 205 | def comment_authors(with: nil, after: nil) 206 | comment_authors = [] 207 | 208 | comments.each do |comment| 209 | next if after and Time.parse(comment.created_at) < after 210 | if (with.nil? or comment[:body] =~ with) 211 | comment_authors |= [comment.user.login] 212 | end 213 | end 214 | 215 | # remove the current user from the list to check 216 | comment_authors -= [self.user.login] 217 | comment_authors.uniq 218 | end 219 | 220 | def get_committed_time(commit_sha) 221 | last_commit = GitReflow.git_server.connection.repos.commits.find GitReflow.git_server.class.remote_user, GitReflow.git_server.class.remote_repo_name, commit_sha 222 | Time.parse last_commit.commit.author[:date] 223 | end 224 | 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/git_reflow/git_server/pull_request.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module GitServer 3 | class PullRequest 4 | attr_accessor :description, :html_url, :feature_branch_name, :base_branch_name, :build, :source_object, :number 5 | 6 | DEFAULT_APPROVAL_REGEX = /(?i-mx:lgtm|looks good to me|:\+1:|:thumbsup:|:shipit:)/ 7 | 8 | class Build 9 | attr_accessor :state, :description, :url 10 | 11 | def initialize(state: nil, description: nil, url: nil) 12 | self.state = state 13 | self.description = description 14 | self.url = url 15 | end 16 | end 17 | 18 | def self.minimum_approvals 19 | "#{GitReflow::Config.get('constants.minimumApprovals')}" 20 | end 21 | 22 | def self.approval_regex 23 | if "#{GitReflow::Config.get('constants.approvalRegex')}".length > 0 24 | Regexp.new("#{GitReflow::Config.get('constants.approvalRegex')}") 25 | else 26 | DEFAULT_APPROVAL_REGEX 27 | end 28 | end 29 | 30 | def initialize(attributes) 31 | raise "PullRequest#initialize must be implemented" 32 | end 33 | 34 | def commit_author 35 | raise "#{self.class.to_s}#commit_author method must be implemented" 36 | end 37 | 38 | def comments 39 | raise "#{self.class.to_s}#comments method must be implemented" 40 | end 41 | 42 | def has_comments? 43 | comments.count > 0 44 | end 45 | 46 | def last_comment 47 | raise "#{self.class.to_s}#last_comment method must be implemented" 48 | end 49 | 50 | def reviewers 51 | raise "#{self.class.to_s}#reviewers method must be implemented" 52 | end 53 | 54 | def approvals 55 | raise "#{self.class.to_s}#approvals method must be implemented" 56 | end 57 | 58 | def reviewers_pending_response 59 | reviewers - approvals 60 | end 61 | 62 | def approved? 63 | has_comments_or_approvals = (has_comments? or approvals.any?) 64 | 65 | case self.class.minimum_approvals 66 | when "0" 67 | true 68 | when "", nil 69 | # Approvals from every commenter 70 | has_comments_or_approvals && reviewers_pending_response.empty? 71 | else 72 | approvals.size >= self.class.minimum_approvals.to_i 73 | end 74 | end 75 | 76 | def build_status 77 | build.nil? ? nil : build.state 78 | end 79 | 80 | def rejection_message 81 | if !build_status.nil? and build_status != "success" 82 | "#{build.description}: #{build.url}" 83 | elsif !approval_minimums_reached? 84 | "You need approval from at least #{self.class.minimum_approvals} users!" 85 | elsif !all_comments_addressed? 86 | # Maybe add what the last comment is? 87 | "The last comment is holding up approval:\n#{last_comment}" 88 | elsif reviewers_pending_response.count > 0 89 | "You still need a LGTM from: #{reviewers_pending_response.join(', ')}" 90 | else 91 | "Your code has not been reviewed yet." 92 | end 93 | end 94 | 95 | def approval_minimums_reached? 96 | self.class.minimum_approvals.length <= 0 or approvals.size >= self.class.minimum_approvals.to_i 97 | end 98 | 99 | def all_comments_addressed? 100 | self.class.minimum_approvals.length <= 0 or !last_comment.match(self.class.approval_regex).nil? 101 | end 102 | 103 | def good_to_merge?(force: false) 104 | return true if force 105 | 106 | (build_status.nil? or build_status == "success") and approved? 107 | end 108 | 109 | def display_pull_request_summary 110 | summary_data = { 111 | "branches" => "#{self.feature_branch_name} -> #{self.base_branch_name}", 112 | "number" => self.number, 113 | "url" => self.html_url 114 | } 115 | 116 | notices = [] 117 | reviewed_by = [] 118 | 119 | # check for CI build status 120 | if self.build_status 121 | notices << "Your build status is not successful: #{self.build.url}.\n" unless self.build.state == "success" 122 | summary_data.merge!( "Build status" => GitReflow.git_server.colorized_build_description(self.build.state, self.build.description) ) 123 | end 124 | 125 | # check for needed lgtm's 126 | if self.reviewers.any? 127 | reviewed_by = self.reviewers.map {|author| author.colorize(:red) } 128 | summary_data.merge!("Last comment" => self.last_comment) 129 | 130 | if self.approvals.any? 131 | reviewed_by.map! { |author| approvals.include?(author.uncolorize) ? author.colorize(:green) : author } 132 | end 133 | 134 | notices << "You still need a LGTM from: #{reviewers_pending_response.join(', ')}\n" if reviewers_pending_response.any? 135 | else 136 | notices << "No one has reviewed your pull request.\n" 137 | end 138 | 139 | summary_data['reviewed by'] = reviewed_by.join(', ') 140 | 141 | padding_size = summary_data.keys.max_by(&:size).size + 2 142 | summary_data.keys.sort.each do |name| 143 | string_format = " %-#{padding_size}s %s\n" 144 | printf string_format, "#{name}:", summary_data[name] 145 | end 146 | 147 | notices.each do |notice| 148 | GitReflow.say notice, :notice 149 | end 150 | end 151 | 152 | def method_missing(method_sym, *arguments, &block) 153 | if source_object and source_object.respond_to? method_sym 154 | source_object.send method_sym 155 | else 156 | super 157 | end 158 | end 159 | 160 | def commit_message_for_merge 161 | return GitReflow.merge_commit_template unless GitReflow.merge_commit_template.nil? 162 | 163 | message = "" 164 | 165 | if "#{self.description}".length > 0 166 | message << "#{self.description}" 167 | else 168 | message << "#{GitReflow.get_first_commit_message}" 169 | end 170 | 171 | message << "\nMerges ##{self.number}\n" 172 | 173 | if lgtm_authors = Array(self.approvals) and lgtm_authors.any? 174 | message << "\nLGTM given by: @#{lgtm_authors.join(', @')}\n" 175 | end 176 | 177 | "#{message}\n" 178 | end 179 | 180 | def cleanup_feature_branch? 181 | cleanup_local_feature_branch? || cleanup_remote_feature_branch? 182 | end 183 | 184 | def cleanup_local_feature_branch? 185 | # backwards compat 186 | always_cleanup_local = GitReflow::Config.get('reflow.always-cleanup').to_s 187 | always_cleanup_local = GitReflow::Config.get('reflow.always-cleanup-local') if always_cleanup_local.empty? 188 | always_cleanup_local == "true" || (ask "Would you like to cleanup your local feature branch? ") =~ /^y/i 189 | end 190 | 191 | def cleanup_remote_feature_branch? 192 | # backwards compat 193 | always_cleanup_remote = GitReflow::Config.get('reflow.always-cleanup').to_s 194 | always_cleanup_remote = GitReflow::Config.get('reflow.always-cleanup-remote') if always_cleanup_remote.empty? 195 | always_cleanup_remote == "true" || (ask "Would you like to cleanup your remote feature branch? ") =~ /^y/i 196 | end 197 | 198 | def deliver? 199 | GitReflow::Config.get('reflow.always-deliver') == "true" || (ask "This is the current status of your Pull Request. Are you sure you want to deliver? ") =~ /^y/i 200 | end 201 | 202 | def cleanup_failure_message 203 | GitReflow.say "Cleanup halted. Local changes were not pushed to remote repo.", :deliver_halted 204 | GitReflow.say "To reset and go back to your branch run \`git reset --hard origin/#{self.base_branch_name} && git checkout #{self.feature_branch_name}\`" 205 | end 206 | 207 | def merge!(options = {}) 208 | if deliver? 209 | 210 | GitReflow.say "Merging pull request ##{self.number}: '#{self.title}', from '#{self.feature_branch_name}' into '#{self.base_branch_name}'", :notice 211 | 212 | GitReflow.update_current_branch 213 | GitReflow.fetch_destination(self.base_branch_name) 214 | 215 | message = commit_message_for_merge 216 | merge_method = options[:merge_method] || GitReflow::Config.get("reflow.merge-method") 217 | merge_method = "squash" if "#{merge_method}".length < 1 218 | 219 | 220 | GitReflow.run_command_with_label "git checkout #{self.base_branch_name}" 221 | GitReflow.run_command_with_label "git pull origin #{self.base_branch_name}" 222 | 223 | case merge_method.to_s 224 | when /squash/i 225 | GitReflow.run_command_with_label "git merge --squash #{self.feature_branch_name}" 226 | else 227 | GitReflow.run_command_with_label "git merge #{self.feature_branch_name}" 228 | end 229 | 230 | GitReflow.append_to_merge_commit_message(message) if message.length > 0 231 | 232 | if GitReflow.run_command_with_label 'git commit', with_system: true 233 | GitReflow.say "Pull request ##{self.number} successfully merged.", :success 234 | 235 | if cleanup_feature_branch? 236 | GitReflow.run_command_with_label "git push origin #{self.base_branch_name}" 237 | GitReflow.run_command_with_label "git push origin :#{self.feature_branch_name}" 238 | GitReflow.run_command_with_label "git branch -D #{self.feature_branch_name}" 239 | GitReflow.say "Nice job buddy." 240 | else 241 | cleanup_failure_message 242 | end 243 | else 244 | GitReflow.say "There were problems commiting your feature... please check the errors above and try again.", :error 245 | end 246 | else 247 | GitReflow.say "Merge aborted", :deliver_halted 248 | end 249 | end 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /lib/git_reflow/logger.rb: -------------------------------------------------------------------------------- 1 | require 'git_reflow/config' 2 | require 'logger' 3 | 4 | module GitReflow 5 | class Logger < ::Logger 6 | DEFAULT_LOG_FILE = "/tmp/git-reflow.log" 7 | COLORS = { 8 | "FATAL" => :red, 9 | "ERROR" => :red, 10 | "WARN" => :orange, 11 | "INFO" => :yellow, 12 | "DEBUG" => :white, 13 | } 14 | 15 | def initialize(*args) 16 | log_file = args.shift || log_file_path 17 | args.unshift(log_file) 18 | super(*args) 19 | @formatter = SimpleFormatter.new 20 | end 21 | 22 | # Simple formatter which only displays the message. 23 | class SimpleFormatter < ::Logger::Formatter 24 | # This method is invoked when a log event occurs 25 | def call(severity, timestamp, progname, msg) 26 | if $stdout.tty? 27 | "#{severity.colorize(COLORS[severity])}: #{String === msg ? msg : msg.inspect}\n" 28 | else 29 | "#{severity}: #{String === msg ? msg : msg.inspect}\n" 30 | end 31 | end 32 | end 33 | 34 | private 35 | 36 | def log_file_path 37 | return @log_file_path if "#{@log_file_path}".length > 0 38 | 39 | # Here we have to run the command in isolation to avoid a recursive loop 40 | # to log this command run to fetch the config setting. 41 | configured_log_file_path = %x{git config --get reflow.log-file-path} 42 | 43 | if configured_log_file_path.length > 0 44 | @log_file_path = configured_log_file_path 45 | else 46 | @log_file_path = DEFAULT_LOG_FILE 47 | end 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/git_reflow/merge_error.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module GitServer 3 | class MergeError < StandardError 4 | def initialize(msg="Merge failed") 5 | super(msg) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/git_reflow/rspec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'rspec/command_line_helpers' 2 | require_relative 'rspec/stub_helpers' 3 | require_relative 'rspec/workflow_helpers' 4 | -------------------------------------------------------------------------------- /lib/git_reflow/rspec/command_line_helpers.rb: -------------------------------------------------------------------------------- 1 | require "highline" 2 | 3 | module GitReflow 4 | module RSpec 5 | # @nodoc 6 | module CommandLineHelpers 7 | def stub_command_line 8 | $commands_ran = [] 9 | $stubbed_commands = {} 10 | $stubbed_runners = Set.new 11 | $output = [] 12 | $says = [] 13 | 14 | stub_run_for GitReflow 15 | stub_run_for GitReflow::Sandbox 16 | stub_run_for GitReflow::Workflow 17 | stub_run_for GitReflow::Workflows::Core if defined? GitReflow::Workflows 18 | 19 | stub_output_for(GitReflow) 20 | stub_output_for(GitReflow::Sandbox) 21 | stub_output_for(GitReflow::Workflow) 22 | 23 | allow_any_instance_of(GitReflow::GitServer::PullRequest).to receive(:printf) do |format, *output| 24 | $output << Array(output).join(" ") 25 | output = '' 26 | end.and_return("") 27 | end 28 | 29 | def stub_output_for(object_to_stub, method_to_stub = :puts) 30 | allow_any_instance_of(object_to_stub).to receive(method_to_stub) do |output| 31 | $output << output 32 | output = '' 33 | end 34 | end 35 | 36 | def stub_run_for(module_to_stub) 37 | $stubbed_runners << module_to_stub 38 | allow(module_to_stub).to receive(:run) do |command, options| 39 | options = { loud: true, blocking: true }.merge(options || {}) 40 | $commands_ran << Hashie::Mash.new(command: command, options: options) 41 | ret_value = $stubbed_commands[command] || "" 42 | command = "" # we need this due to a bug in rspec that will keep this assignment on subsequent runs of the stub 43 | ret_value 44 | end 45 | allow(module_to_stub).to receive(:say) do |output, type| 46 | $says << {message: output, type: type} 47 | end 48 | end 49 | 50 | def reset_stubbed_command_line 51 | $commands_ran = [] 52 | $stubbed_commands = {} 53 | $output = [] 54 | $says = [] 55 | end 56 | 57 | def stub_command(command:, return_value: "", options: {}) 58 | $stubbed_commands[command] = return_value 59 | $stubbed_runners.each do |runner| 60 | allow(runner).to receive(:run).with(command, options) do |command, options| 61 | options = { loud: true, blocking: true }.merge(options || {}) 62 | $commands_ran << Hashie::Mash.new(command: command, options: options) 63 | $stubbed_commands[command] = return_value 64 | raise GitReflow::Sandbox::CommandError.new(return_value, "\"#{command}\" failed to run.") if options[:raise] 65 | end 66 | end 67 | end 68 | 69 | def stub_command_line_inputs_for(module_to_stub, inputs) 70 | allow(module_to_stub).to receive(:ask) do |terminal, question| 71 | return_value = inputs[question] 72 | question = "" 73 | return_value 74 | end 75 | end 76 | 77 | def stub_command_line_inputs(inputs) 78 | allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question| 79 | return_value = inputs[question] 80 | question = "" 81 | return_value 82 | end 83 | end 84 | 85 | end 86 | end 87 | end 88 | 89 | RSpec::Matchers.define :have_run_command do |command, options| 90 | options = { blocking: true, loud: true }.merge(options || {}) 91 | 92 | match do |block| 93 | block.call 94 | ( 95 | $commands_ran.include? Hashie::Mash.new(command: command, options: options) or 96 | $commands_ran.include? Hashie::Mash.new(command: command, options: options.merge({with_system: true})) 97 | ) 98 | end 99 | 100 | supports_block_expectations 101 | 102 | failure_message do |block| 103 | "expected to have run the command \`#{command}\` with options \`#{options}\` but instead ran:\n\t#{$commands_ran.inspect}" 104 | end 105 | end 106 | 107 | RSpec::Matchers.define :have_run_command_silently do |command, options| 108 | options = { blocking: true, loud: false }.merge(options || {}) 109 | 110 | match do |block| 111 | block.call 112 | $commands_ran.include? Hashie::Mash.new(command: command, options: options) 113 | end 114 | 115 | supports_block_expectations 116 | 117 | failure_message do |block| 118 | "expected to have run the command \`#{command}\` silently with options \`#{options}\` but instead ran:\n\t#{$commands_ran.inspect}" 119 | end 120 | end 121 | 122 | RSpec::Matchers.define :have_run_commands_in_order do |commands| 123 | match do |block| 124 | block.call 125 | remaining_commands = commands 126 | command_start_index = $commands_ran.find_index {|c| c.command == commands.first } 127 | return false unless command_start_index 128 | 129 | $commands_ran.each_with_index do |command_ran, index| 130 | # seek to starting point of first command to match 131 | next unless index >= command_start_index 132 | if remaining_commands.size > 0 133 | expect(remaining_commands[0]).to eq(command_ran.command) 134 | remaining_commands.shift 135 | end 136 | end 137 | 138 | return remaining_commands.count == 0 139 | end 140 | 141 | supports_block_expectations 142 | 143 | failure_message do |block| 144 | "expected to have run these commands in order:\n\t\t#{commands.inspect}\n\tgot:\n\t\t#{$commands_ran.map(&:command).inspect}" 145 | end 146 | end 147 | 148 | RSpec::Matchers.define :have_said do |expected_message, expected_type| 149 | match do |block| 150 | block.call 151 | $says.include?({message: expected_message, type: expected_type}) 152 | end 153 | 154 | supports_block_expectations 155 | 156 | failure_message do |block| 157 | "expected GitReflow to have said #{expected_message} with #{expected_type.inspect} but didn't: \n\t#{$says.inspect}" 158 | end 159 | end 160 | 161 | RSpec::Matchers.define :have_output do |expected_output| 162 | match do |block| 163 | block.call 164 | $output.join("\n").include? expected_output 165 | end 166 | 167 | supports_block_expectations 168 | 169 | failure_message do |block| 170 | "expected STDOUT to include #{expected_output} but didn't: \n\t#{$output.inspect}" 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/git_reflow/rspec/stub_helpers.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module RSpec 3 | module StubHelpers 4 | 5 | def stub_with_fallback(obj, method) 6 | original_method = obj.method(method) 7 | allow(obj).to receive(method).with(anything()) { |*args| original_method.call(*args) } 8 | return allow(obj).to receive(method) 9 | end 10 | 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/git_reflow/rspec/workflow_helpers.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module RSpec 3 | # @nodoc 4 | module WorkflowHelpers 5 | def use_workflow(path) 6 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return( 7 | GitReflow::Workflows::Core.load_raw_workflow(File.read(path)) 8 | ) 9 | end 10 | 11 | def suppress_loading_of_external_workflows 12 | allow(GitReflow::Workflows::Core).to receive(:load__workflow).with("#{GitReflow.git_root_dir}/Workflow").and_return(false) 13 | return if GitReflow::Config.get('reflow.workflow').to_s.empty? 14 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).with(GitReflow::Config.get('reflow.workflow')).and_return(false) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/git_reflow/sandbox.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | module Sandbox 3 | extend self 4 | 5 | COLOR_FOR_LABEL = { 6 | notice: :yellow, 7 | info: :yellow, 8 | error: :red, 9 | deliver_halted: :red, 10 | review_halted: :red, 11 | success: :green, 12 | plain: :white 13 | } 14 | 15 | class CommandError < StandardError; 16 | attr_reader :output 17 | def initialize(output, *args) 18 | @output = output 19 | super(*args) 20 | end 21 | end 22 | 23 | def run(command, options = {}) 24 | options = { loud: true, blocking: true, raise: false }.merge(options) 25 | 26 | GitReflow.logger.debug "Running... #{command}" 27 | 28 | if options[:with_system] == true 29 | system(command) 30 | else 31 | output = %x{#{command}} 32 | 33 | if !$?.success? 34 | raise CommandError.new(output, "\"#{command}\" failed to run.") if options[:raise] == true 35 | abort "\"#{command}\" failed to run." if options[:blocking] == true 36 | end 37 | 38 | puts output if options[:loud] == true 39 | output 40 | end 41 | end 42 | 43 | def run_command_with_label(command, options = {}) 44 | label_color = options.delete(:color) || :green 45 | puts command.colorize(label_color) 46 | run(command, options) 47 | end 48 | 49 | def say(message, label_type = :plain) 50 | if COLOR_FOR_LABEL[label_type] 51 | label = (label_type.to_s == "plain") ? "" : "[#{ label_type.to_s.gsub('_', ' ').colorize(COLOR_FOR_LABEL[label_type]) }] " 52 | puts "#{label}#{message}" 53 | else 54 | puts message 55 | end 56 | end 57 | 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/git_reflow/version.rb: -------------------------------------------------------------------------------- 1 | module GitReflow 2 | VERSION = "0.9.9" 3 | end 4 | -------------------------------------------------------------------------------- /lib/git_reflow/workflow.rb: -------------------------------------------------------------------------------- 1 | require 'git_reflow/logger' 2 | require 'git_reflow/sandbox' 3 | require 'git_reflow/git_helpers' 4 | require 'bundler/inline' 5 | 6 | module GitReflow 7 | module Workflow 8 | def self.included base 9 | base.extend ClassMethods 10 | end 11 | 12 | # @nodoc 13 | def self.current 14 | return @current unless @current.nil? 15 | # First look for a "Workflow" file in the current directory, then check 16 | # for a global Workflow file stored in git-reflow git config. 17 | loaded_local_workflow = GitReflow::Workflows::Core.load_workflow "#{GitReflow.git_root_dir}/Workflow" 18 | loaded_global_workflow = false 19 | 20 | unless loaded_local_workflow 21 | loaded_global_workflow = GitReflow::Workflows::Core.load_workflow GitReflow::Config.get('reflow.workflow') 22 | end 23 | 24 | @current = GitReflow::Workflows::Core 25 | end 26 | 27 | # @nodoc 28 | # This is primarily a helper method for tests. Due to the nature of how the 29 | # tests load many different workflows, this helps start fresh and isolate 30 | # the scenario at hand. 31 | def self.reset! 32 | GitReflow.logger.debug "Resetting GitReflow workflow..." 33 | current.commands = {} 34 | current.callbacks = { before: {}, after: {}} 35 | @current = nil 36 | # We'll need to reload the core class again in order to clear previously 37 | # eval'd content in the context of the class 38 | load File.expand_path('../workflows/core.rb', __FILE__) 39 | end 40 | 41 | module ClassMethods 42 | include GitReflow::Sandbox 43 | include GitReflow::GitHelpers 44 | 45 | def commands 46 | @commands ||= {} 47 | end 48 | 49 | def commands=(command_hash) 50 | @commands = command_hash 51 | end 52 | 53 | def command_docs 54 | @command_docs ||= {} 55 | end 56 | 57 | def command_docs=(command_doc_hash) 58 | @command_docs = command_doc_hash 59 | end 60 | 61 | def callbacks 62 | @callbacks ||= { 63 | before: {}, 64 | after: {} 65 | } 66 | end 67 | 68 | def callbacks=(callback_hash) 69 | @callbacks = callback_hash 70 | end 71 | 72 | # Proxy our Config class so that it's available in workflow files 73 | def git_config 74 | GitReflow::Config 75 | end 76 | 77 | def git_server 78 | GitReflow.git_server 79 | end 80 | 81 | def logger(*args) 82 | return @logger if defined?(@logger) 83 | 84 | @logger = GitReflow.try(:logger, *args) || GitReflow::Logger.new(*args) 85 | rescue NoMethodError 86 | @logger = GitReflow::Logger.new(*args) 87 | end 88 | 89 | # Checks for an installed gem, and if none is installed use bundler's 90 | # inline gemfile to install it. 91 | # 92 | # @param name [String] the name of the gem to require as a dependency 93 | def use_gem(name, *args) 94 | run("gem list -ie #{name}", loud: false, raise: true) 95 | logger.info "Using installed gem '#{name}' with options: #{args.inspect}" 96 | rescue ::GitReflow::Sandbox::CommandError => e 97 | abort e.message unless e.output =~ /\Afalse/ 98 | logger.info "Installing gem '#{name}' with options: #{args.inspect}" 99 | say "Installing gem '#{name}'...", :notice 100 | gemfile do 101 | source "https://rubygems.org" 102 | gem name, *args 103 | end 104 | end 105 | 106 | # Use bundler's inline gemfile to install dependencies. 107 | # See: https://bundler.io/v1.16/guides/bundler_in_a_single_file_ruby_script.html 108 | # 109 | # @yield A block to be executed in the context of Bundler's `gemfile` DSL 110 | def use_gemfile(&block) 111 | logger.info "Using a custom gemfile" 112 | gemfile(true, &block) 113 | end 114 | 115 | # Loads a pre-defined workflow (FlatMergeWorkflow) from within another 116 | # Workflow file 117 | # 118 | # @param name [String] the name of the Workflow file to use as a basis 119 | def use(workflow_name) 120 | if workflows.key?(workflow_name) 121 | logger.debug "Using Workflow: #{workflow_name}" 122 | GitReflow::Workflows::Core.load_workflow(workflows[workflow_name]) 123 | else 124 | logger.error "Tried to use non-existent Workflow: #{workflow_name}" 125 | end 126 | end 127 | 128 | # Keeps track of available workflows when using `.use(workflow_name)` 129 | # Workflow file 130 | # 131 | # @return [Hash, nil] A hash with [workflow_name, workflow_path] as key/value pairs 132 | def workflows 133 | return @workflows if @workflows 134 | workflow_paths = Dir["#{File.dirname(__FILE__)}/workflows/*Workflow"] 135 | @workflows = {} 136 | workflow_paths.each { |p| @workflows[File.basename(p)] = p } 137 | @workflows 138 | end 139 | 140 | # Creates a singleton method on the included class 141 | # 142 | # This method will take any number of keyword parameters. If @defaults keyword is provided, and the given 143 | # key(s) in the defaults are not provided as keyword parameters, then it will use the value given in the 144 | # defaults for that parameter. 145 | # 146 | # @param name [Symbol] the name of the method to create 147 | # @param defaults [Hash] keyword arguments to provide fallbacks for 148 | # 149 | # @yield [a:, b:, c:, ...] Invokes the block with an arbitrary number of keyword arguments 150 | def command(name, **params, &block) 151 | params[:flags] ||= {} 152 | params[:switches] ||= {} 153 | params[:arguments] ||= {} 154 | defaults ||= params[:arguments].merge(params[:flags]).merge(params[:switches]) 155 | 156 | # Ensure flags and switches use kebab-case 157 | kebab_case_keys!(params[:flags]) 158 | kebab_case_keys!(params[:switches]) 159 | 160 | # Register the command with the workflow so that we can properly handle 161 | # option parsing from the command line 162 | self.commands[name] = params 163 | self.command_docs[name] = params 164 | 165 | logger.debug "adding new command '#{name}' with #{defaults.inspect}" 166 | self.define_singleton_method(name) do |args = {}| 167 | args_with_defaults = {} 168 | args.each do |name, value| 169 | if "#{value}".length <= 0 && !defaults[name].nil? 170 | args_with_defaults[name] = defaults[name] 171 | else 172 | args_with_defaults[name] = value 173 | end 174 | end 175 | 176 | defaults.each do |name, value| 177 | if "#{args_with_defaults[name]}".length <= 0 178 | args_with_defaults[name] = value 179 | end 180 | end 181 | 182 | logger.debug "callbacks: #{callbacks.inspect}" 183 | Array(callbacks[:before][name]).each do |block| 184 | logger.debug "(before) callback running for `#{name}` command..." 185 | argument_overrides = block.call(**args_with_defaults) || {} 186 | args_with_defaults.merge!(argument_overrides) if argument_overrides.is_a?(Hash) 187 | end 188 | 189 | logger.info "Running command `#{name}` with args: #{args_with_defaults.inspect}..." 190 | block.call(**args_with_defaults) 191 | 192 | Array(callbacks[:after][name]).each do |block| 193 | logger.debug "(after) callback running for `#{name}` command..." 194 | block.call(**args_with_defaults) 195 | end 196 | end 197 | end 198 | 199 | # Stores a Proc to be called once the command successfully finishes 200 | # 201 | # Procs declared with `before` are executed sequentially in the order they are defined in a custom Workflow 202 | # file. 203 | # 204 | # @param name [Symbol] the name of the method to create 205 | # 206 | # @yield A block to be executed before the given command. These blocks 207 | # are executed in the context of `GitReflow::Workflows::Core` 208 | def before(name, &block) 209 | name = name.to_sym 210 | if commands[name].nil? 211 | logger.error "Attempted to register (before) callback for non-existing command: #{name}" 212 | else 213 | logger.debug "(before) callback registered for: #{name}" 214 | callbacks[:before][name] ||= [] 215 | callbacks[:before][name] << block 216 | end 217 | end 218 | 219 | # Stores a Proc to be called once the command successfully finishes 220 | # 221 | # Procs declared with `after` are executed sequentially in the order they are defined in a custom Workflow 222 | # file. 223 | # 224 | # @param name [Symbol] the name of the method to create 225 | # 226 | # @yield A block to be executed after the given command. These blocks 227 | # are executed in the context of `GitReflow::Workflows::Core` 228 | def after(name, &block) 229 | name = name.to_sym 230 | if commands[name].nil? 231 | logger.error "Attempted to register (after) callback for non-existing command: #{name}" 232 | else 233 | logger.debug "(after) callback registered for: #{name}" 234 | callbacks[:after][name] ||= [] 235 | callbacks[:after][name] << block 236 | end 237 | end 238 | 239 | # Creates a singleton method on the included class 240 | # 241 | # This method updates the help text associated with the provided command. 242 | # 243 | # @param name [Symbol] the name of the command to add/update help text for 244 | # @param defaults [Hash] keyword arguments to provide fallbacks for 245 | def command_help(name, summary:, arguments: {}, flags: {}, switches: {}, description: "") 246 | command_docs[name] = { 247 | summary: summary, 248 | description: description, 249 | arguments: arguments, 250 | flags: kebab_case_keys!(flags), 251 | switches: kebab_case_keys!(switches) 252 | } 253 | end 254 | 255 | # Outputs documentation for the provided command 256 | # 257 | # @param name [Symbol] the name of the command to output help text for 258 | def documentation_for_command(name) 259 | name = name.to_sym 260 | docs = command_docs[name] 261 | if !docs.nil? 262 | GitReflow.say "USAGE" 263 | GitReflow.say " git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}" 264 | if docs[:arguments].any? 265 | GitReflow.say "ARGUMENTS" 266 | docs[:arguments].each do |arg_name, arg_desc| 267 | default_text = commands[name][:arguments][arg_name].nil? ? "" : "(default: #{commands[name][:arguments][arg_name]}) " 268 | GitReflow.say " #{arg_name} – #{default_text}#{arg_desc}" 269 | end 270 | end 271 | if docs[:flags].any? || docs[:switches].any? 272 | cmd = commands[name.to_sym] 273 | GitReflow.say "COMMAND OPTIONS" 274 | docs[:flags].each do |flag_name, flag_desc| 275 | flag_names = ["-#{flag_name.to_s[0]}", "--#{flag_name}"] 276 | flag_default = cmd[:flags][flag_name] 277 | 278 | GitReflow.say " #{flag_names} – #{!flag_default.nil? ? "(default: #{flag_default}) " : ""}#{flag_desc}" 279 | end 280 | docs[:switches].each do |switch_name, switch_desc| 281 | switch_names = [switch_name.to_s[0], "-#{switch_name}"].map {|s| "-#{s}" }.join(', ') 282 | switch_default = cmd[:switches][switch_name] 283 | 284 | GitReflow.say " #{switch_names} – #{!switch_default.nil? ? "(default: #{switch_default}) " : ""}#{switch_desc}" 285 | end 286 | end 287 | else 288 | help 289 | end 290 | end 291 | 292 | # Outputs documentation for git-reflow 293 | def help 294 | GitReflow.say "NAME" 295 | GitReflow.say " git-reflow – Git Reflow manages your git workflow." 296 | GitReflow.say "VERSION" 297 | GitReflow.say " #{GitReflow::VERSION}" 298 | GitReflow.say "USAGE" 299 | GitReflow.say " git-reflow command [command options] [arguments...]" 300 | GitReflow.say "COMMANDS" 301 | command_docs.each do |command_name, command_doc| 302 | GitReflow.say " #{command_name}\t– #{command_doc[:summary]}" 303 | end 304 | end 305 | 306 | # Parses ARGV for the provided git-reflow command name 307 | # 308 | # @param name [Symbol, String] the name of the git-reflow command to parse from ARGV 309 | def parse_command_options!(name) 310 | name = name.to_sym 311 | options = {} 312 | docs = command_docs[name] 313 | OptionParser.new do |opts| 314 | opts.banner = "USAGE:\n git-reflow #{name} [command options] #{docs[:arguments].keys.map {|arg| "[#{arg}]" }.join(' ')}" 315 | opts.separator "" 316 | opts.separator "COMMAND OPTIONS:" if docs[:flags].any? || docs[:switches].any? 317 | 318 | self.commands[name][:flags].each do |flag_name, flag_default| 319 | # There is a bug in Ruby that will not parse the flag value if no 320 | # help text is provided. Fallback to the flag name. 321 | flag_help = command_docs[name][:flags][flag_name] || flag_name 322 | opts.on("-#{flag_name[0]}", "--#{flag_name} #{flag_name.upcase}", flag_help) do |f| 323 | options[kebab_to_underscore(flag_name)] = f || flag_default 324 | end 325 | end 326 | 327 | self.commands[name][:switches].each do |switch_name, switch_default| 328 | # There is a bug in Ruby that will not parse the switch value if no 329 | # help text is provided. Fallback to the switch name. 330 | switch_help = command_docs[name][:switches][switch_name] || switch_name 331 | opts.on("-#{switch_name[0]}", "--[no-]#{switch_name}", switch_help) do |s| 332 | options[kebab_to_underscore(switch_name)] = s || switch_default 333 | end 334 | end 335 | end.parse! 336 | 337 | # Add arguments to optiosn to pass to defined commands 338 | commands[name][:arguments].each do |arg_name, arg_default| 339 | options[arg_name] = ARGV.shift || arg_default 340 | end 341 | options 342 | rescue OptionParser::InvalidOption 343 | documentation_for_command(name) 344 | exit 1 345 | end 346 | 347 | private 348 | 349 | def kebab_case_keys!(hsh) 350 | hsh.keys.each do |key_to_update| 351 | hsh[underscore_to_kebab(key_to_update)] = hsh.delete(key_to_update) if key_to_update =~ /_/ 352 | end 353 | 354 | hsh 355 | end 356 | 357 | def kebab_to_underscore(sym_or_string) 358 | sym_or_string.to_s.gsub('-', '_').to_sym 359 | end 360 | 361 | def underscore_to_kebab(sym_or_string) 362 | sym_or_string.to_s.gsub('_', '-').to_sym 363 | end 364 | end 365 | end 366 | end 367 | 368 | extend GitReflow::Workflow 369 | -------------------------------------------------------------------------------- /lib/git_reflow/workflows/FlatMergeWorkflow: -------------------------------------------------------------------------------- 1 | command(:deliver, arguments: { base: "master" }, flags: { merge_method: "merge" }, switches: { force: false, skip_lgtm: false }) do |**params| 2 | params[:force] = params[:force] || params[:skip_lgtm] 3 | begin 4 | existing_pull_request = git_server.find_open_pull_request( from: current_branch, to: params[:base] ) 5 | 6 | if existing_pull_request.nil? 7 | say "No pull request exists for #{remote_user}:#{current_branch}\nPlease submit your branch for review first with \`git reflow review\`", :deliver_halted 8 | else 9 | 10 | if existing_pull_request.good_to_merge?(force: params[:force]) 11 | # displays current status and prompts user for confirmation 12 | self.status destination_branch: params[:base] 13 | existing_pull_request.merge!(params) 14 | else 15 | say existing_pull_request.rejection_message, :deliver_halted 16 | end 17 | 18 | end 19 | 20 | rescue Github::Error::UnprocessableEntity => e 21 | say "Github Error: #{e.inspect}", :error 22 | end 23 | end 24 | command_help( 25 | :deliver, 26 | summary: "deliver your feature branch", 27 | arguments: { 28 | base: "the branch to merge this feature into" 29 | }, 30 | flags: { 31 | merge_method: "how you want your feature branch merged ('merge', 'squash', 'rebase')" 32 | }, 33 | switches: { 34 | force: "skip the lgtm checks and deliver your feature branch", 35 | skip_lgtm: "skip the lgtm checks and deliver your feature branch" 36 | }, 37 | description: "merge your feature branch down to your base branch, and cleanup your feature branch" 38 | ) 39 | -------------------------------------------------------------------------------- /spec/fixtures/authentication_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "error" : "GET https://api.github.com/authorizations: 401 Bad credentials" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/awesome_workflow.rb: -------------------------------------------------------------------------------- 1 | command :start do 2 | say "Awesome." 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/git/git_config: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Reenhanced 3 | email = dev@reenhanced.com 4 | [github] 5 | user = reenhanced 6 | token = 123456 7 | oauth-token = 123456 8 | -------------------------------------------------------------------------------- /spec/fixtures/issues/comment.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "id": <%= id || 1 %>, 3 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/issues/comments/<%= pull_request_number %>", 4 | "html_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/issues/<%= pull_request_number %>#issuecomment-1", 5 | "body": "<%= body || "Hmmm..." %>", 6 | "user": { 7 | "login": "<%= author %>", 8 | "id": 1, 9 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 10 | "gravatar_id": "somehexcode", 11 | "url": "https://api.github.com/users/<%= author %>", 12 | "html_url": "https://github.com/<%= author %>", 13 | "followers_url": "https://api.github.com/users/<%= author %>/followers", 14 | "following_url": "https://api.github.com/users/<%= author %>/following{/other_user}", 15 | "gists_url": "https://api.github.com/users/<%= author %>/gists{/gist_id}", 16 | "starred_url": "https://api.github.com/users/<%= author %>/starred{/owner}{/repo}", 17 | "subscriptions_url": "https://api.github.com/users/<%= author %>/subscriptions", 18 | "organizations_url": "https://api.github.com/users/<%= author %>/orgs", 19 | "repos_url": "https://api.github.com/users/<%= author %>/repos", 20 | "events_url": "https://api.github.com/users/<%= author %>/events{/privacy}", 21 | "received_events_url": "https://api.github.com/users/<%= author %>/received_events", 22 | "type": "User", 23 | "site_admin": false 24 | }, 25 | "created_at": "<%= created_at %>", 26 | "updated_at": "2011-04-14T16:00:49Z" 27 | } 28 | -------------------------------------------------------------------------------- /spec/fixtures/issues/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "url": "https://api.github.com/repos/reenhanced/repo/issues/comments/1", 5 | "html_url": "https://github.com/reenhanced/repo/issues/1#issuecomment-1", 6 | "body": "Me too", 7 | "user": { 8 | "login": "reenhanced", 9 | "id": 1, 10 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 11 | "gravatar_id": "somehexcode", 12 | "url": "https://api.github.com/users/reenhanced", 13 | "html_url": "https://github.com/reenhanced", 14 | "followers_url": "https://api.github.com/users/reenhanced/followers", 15 | "following_url": "https://api.github.com/users/reenhanced/following{/other_user}", 16 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 17 | "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 18 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 19 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 20 | "repos_url": "https://api.github.com/users/reenhanced/repos", 21 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 22 | "received_events_url": "https://api.github.com/users/reenhanced/received_events", 23 | "type": "User", 24 | "site_admin": false 25 | }, 26 | "created_at": "2011-04-14T16:00:49Z", 27 | "updated_at": "2011-04-14T16:00:49Z" 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /spec/fixtures/issues/comments.json.erb: -------------------------------------------------------------------------------- 1 | [ 2 | <% comment_json = [] %> 3 | <% comments.each_with_index do |comment, index| %> 4 | <% comment_json << Fixture.new('issues/comment.json.erb', 5 | id: comment[:id] || index + 1, 6 | author: comment[:author], 7 | pull_request_number: pull_request_number, 8 | repo_owner: repo_owner, 9 | repo_name: repo_name, 10 | body: comment[:body] || 'Hmmm...', 11 | created_at: comment[:created_at] || '2011-04-14T16:00:49Z' 12 | ).to_s %> 13 | <% end %> 14 | <%= comment_json.join(", ") %> 15 | ] 16 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/comment.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/pulls/comments/<%= id || 1 %>", 3 | "id": <%= id || 1 %>, 4 | "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", 5 | "path": "file1.txt", 6 | "position": 1, 7 | "original_position": 4, 8 | "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 9 | "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", 10 | "user": { 11 | "login": "<%= author %>", 12 | "id": 1, 13 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 14 | "gravatar_id": "somehexcode", 15 | "url": "https://api.github.com/users/<%= author %>", 16 | "html_url": "https://github.com/<%= author %>", 17 | "followers_url": "https://api.github.com/users/<%= author %>/followers", 18 | "following_url": "https://api.github.com/users/<%= author %>/following{/other_user}", 19 | "gists_url": "https://api.github.com/users/<%= author %>/gists{/gist_id}", 20 | "starred_url": "https://api.github.com/users/<%= author %>/starred{/owner}{/repo}", 21 | "subscriptions_url": "https://api.github.com/users/<%= author %>/subscriptions", 22 | "organizations_url": "https://api.github.com/users/<%= author %>/orgs", 23 | "repos_url": "https://api.github.com/users/<%= author %>/repos", 24 | "events_url": "https://api.github.com/users/<%= author %>/events{/privacy}", 25 | "received_events_url": "https://api.github.com/users/<%= author %>/received_events", 26 | "type": "User", 27 | "site_admin": false 28 | }, 29 | "body": "<%= body || "Great stuff" %>", 30 | "created_at": "<%= created_at || "2011-04-14T16:00:49Z" %>", 31 | "updated_at": "<%= updated_at || created_at || "2011-04-14T16:00:49Z" %>", 32 | "html_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pull/<%= pull_request_number %>#discussion-diff-<%= id || 1 %>", 33 | "pull_request_url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/pulls/<%= pull_request_number %>", 34 | "_links": { 35 | "self": { 36 | "href": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/pulls/comments/<%= pull_request_number %>" 37 | }, 38 | "html": { 39 | "href": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pull/<%= pull_request_number %>#discussion-diff-<%= id || 1 %>" 40 | }, 41 | "pull_request": { 42 | "href": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/pulls/<%= pull_request_number %>" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/repos/reenhanced/repo/pulls/comments/1", 4 | "id": 1, 5 | "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", 6 | "path": "file1.txt", 7 | "position": 1, 8 | "original_position": 4, 9 | "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 10 | "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", 11 | "user": { 12 | "login": "reenhanced", 13 | "id": 1, 14 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 15 | "gravatar_id": "somehexcode", 16 | "url": "https://api.github.com/users/reenhanced", 17 | "html_url": "https://github.com/reenhanced", 18 | "followers_url": "https://api.github.com/users/reenhanced/followers", 19 | "following_url": "https://api.github.com/users/reenhanced/following{/other_user}", 20 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 21 | "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 22 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 23 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 24 | "repos_url": "https://api.github.com/users/reenhanced/repos", 25 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 26 | "received_events_url": "https://api.github.com/users/reenhanced/received_events", 27 | "type": "User", 28 | "site_admin": false 29 | }, 30 | "body": "Great stuff", 31 | "created_at": "2011-04-14T16:00:49Z", 32 | "updated_at": "2011-04-14T16:00:49Z", 33 | "html_url": "https://github.com/reenhanced/repo/pull/1#discussion-diff-1", 34 | "pull_request_url": "https://api.github.com/repos/reenhanced/repo/pulls/1", 35 | "_links": { 36 | "self": { 37 | "href": "https://api.github.com/repos/reenhanced/repo/pulls/comments/1" 38 | }, 39 | "html": { 40 | "href": "https://github.com/reenhanced/repo/pull/1#discussion-diff-1" 41 | }, 42 | "pull_request": { 43 | "href": "https://api.github.com/repos/reenhanced/repo/pulls/1" 44 | } 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/comments.json.erb: -------------------------------------------------------------------------------- 1 | [ 2 | <% comments_json = [] %> 3 | <% comments.each_with_index do |comment, index| %> 4 | <% comments_json << Fixture.new('pull_requests/comment.json.erb', 5 | id: index, 6 | author: comment[:author], 7 | pull_request_number: pull_request_number, 8 | repo_owner: repo_owner, 9 | repo_name: repo_name, 10 | body: comment[:body] || 'Hammer time', 11 | created_at: comment[:created_at] || Chronic.parse('October 21, 2015 07:28:00') 12 | ).to_s %> 13 | <% end %> 14 | <%= comments_json.join(", ") %> 15 | ] 16 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/commits.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "commit": { 4 | "sha": "7638417db6d59f3c431d3e1f261cc637155684cd", 5 | "url": "https://api.github.com/repos/reenhanced/repo/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd", 6 | "author": { 7 | "date": "2010-04-10T14:10:01-07:00", 8 | "name": "Draco Powers", 9 | "email": "draco@reenhanced.com" 10 | }, 11 | "committer": { 12 | "date": "2010-04-10T14:10:01-07:00", 13 | "name": "Draco Powers", 14 | "email": "draco@reenhanced.com" 15 | }, 16 | "message": "added readme, because im a good github citizen\n", 17 | "tree": { 18 | "url": "https://api.github.com/repos/reenhanced/repo/git/trees/691272480426f78a0138979dd3ce63b77f706feb", 19 | "sha": "691272480426f78a0138979dd3ce63b77f706feb" 20 | }, 21 | "parents": [ 22 | { 23 | "url": "https://api.github.com/repos/reenhanced/repo/git/commits/1acc419d4d6a9ce985db7be48c6349a0475975b5", 24 | "sha": "1acc419d4d6a9ce985db7be48c6349a0475975b5" 25 | } 26 | ] 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/external_pull_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/reenhanced/repo/pulls/2", 3 | "html_url": "https://github.com/reenhanced/repo/pulls/2", 4 | "diff_url": "https://github.com/reenhanced/repo/pulls/2.diff", 5 | "patch_url": "https://github.com/reenhanced/repo/pulls/2.patch", 6 | "issue_url": "https://github.com/reenhanced/repo/issue/2", 7 | "number": 2, 8 | "state": "open", 9 | "title": "new-external-feature", 10 | "body": "Please pull these awesome changes", 11 | "created_at": "2011-01-26T19:01:12Z", 12 | "updated_at": "2011-01-26T19:01:12Z", 13 | "closed_at": "2011-01-26T19:01:12Z", 14 | "merged_at": "2011-01-26T19:01:12Z", 15 | "_links": { 16 | "self": { 17 | "href": "https://api.github.com/reenhanced/repo/pulls/2" 18 | }, 19 | "html": { 20 | "href": "https://github.com/reenhanced/repo/pull/2" 21 | }, 22 | "comments": { 23 | "href": "https://api.github.com/reenhanced/repo/issues/2/comments" 24 | }, 25 | "review_comments": { 26 | "href": "https://api.github.com/reenhanced/repo/pulls/2/comments" 27 | } 28 | }, 29 | "merged": false, 30 | "mergeable": true, 31 | "merged_by": { 32 | "login": "reenhanced", 33 | "id": 1, 34 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 35 | "gravatar_id": "somehexcode", 36 | "url": "https://api.github.com/users/reenhanced" 37 | }, 38 | "comments": 10, 39 | "commits": 3, 40 | "additions": 100, 41 | "deletions": 3, 42 | "changed_files": 5, 43 | "user": { 44 | "login": "reenhanced", 45 | "id": 1, 46 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 47 | "gravatar_id": "", 48 | "url": "https://api.github.com/users/reenhanced", 49 | "html_url": "https://github.com/reenhanced", 50 | "followers_url": "https://api.github.com/users/reenhanced/followers", 51 | "following_url": 52 | "https://api.github.com/users/reenhanced/following{/other_user}", 53 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 54 | "starred_url": 55 | "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 56 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 57 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 58 | "repos_url": "https://api.github.com/users/reenhanced/repos", 59 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 60 | "received_events_url": 61 | "https://api.github.com/users/reenhanced/received_events", 62 | "type": "User", 63 | "site_admin": false 64 | }, 65 | "head": { 66 | "label": "new-external-feature", 67 | "ref": "new-external-feature", 68 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 69 | "user": { 70 | "login": "octocat", 71 | "id": 1, 72 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 73 | "gravatar_id": "somehexcode", 74 | "url": "https://api.github.com/users/octocat" 75 | }, 76 | "repo": { 77 | "url": "https://api.github.com/repos/octocat/repo", 78 | "html_url": "https://github.com/octocat/repo", 79 | "clone_url": "https://github.com/octocat/repo.git", 80 | "git_url": "git://github.com/octocat/repo.git", 81 | "ssh_url": "git@github.com:octocat/repo.git", 82 | "svn_url": "https://svn.github.com/octocat/repo", 83 | "owner": { 84 | "login": "octocat", 85 | "id": 1, 86 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 87 | "gravatar_id": "somehexcode", 88 | "url": "https://api.github.com/users/octocat" 89 | }, 90 | "name": "repo", 91 | "description": "This your first repo!", 92 | "homepage": "https://github.com", 93 | "language": null, 94 | "private": false, 95 | "fork": false, 96 | "forks": 9, 97 | "watchers": 80, 98 | "size": 108, 99 | "master_branch": "master", 100 | "open_issues": 0, 101 | "pushed_at": "2011-01-26T19:06:43Z", 102 | "created_at": "2011-01-26T19:01:12Z" 103 | } 104 | }, 105 | "base": { 106 | "label": "reenhanced:master", 107 | "ref": "master", 108 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 109 | "user": { 110 | "login": "reenhanced", 111 | "id": 1, 112 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 113 | "gravatar_id": "somehexcode", 114 | "url": "https://api.github.com/users/reenhanced" 115 | }, 116 | "repo": { 117 | "url": "https://api.github.com/repos/reenhanced/repo", 118 | "html_url": "https://github.com/reenhanced/repo", 119 | "clone_url": "https://github.com/reenhanced/repo.git", 120 | "git_url": "git://github.com/reenhanced/repo.git", 121 | "ssh_url": "git@github.com:reenhanced/repo.git", 122 | "svn_url": "https://svn.github.com/reenhanced/repo", 123 | "owner": { 124 | "login": "reenhanced", 125 | "id": 1, 126 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 127 | "gravatar_id": "somehexcode", 128 | "url": "https://api.github.com/users/reenhanced" 129 | }, 130 | "name": "repo", 131 | "description": "This your first repo!", 132 | "homepage": "https://github.com", 133 | "language": null, 134 | "private": false, 135 | "fork": false, 136 | "forks": 9, 137 | "watchers": 80, 138 | "size": 108, 139 | "master_branch": "master", 140 | "open_issues": 0, 141 | "pushed_at": "2011-01-26T19:06:43Z", 142 | "created_at": "2011-01-26T19:01:12Z" 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/pull_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/reenhanced/repo/pulls/1", 3 | "html_url": "https://github.com/reenhanced/repo/pulls/1", 4 | "diff_url": "https://github.com/reenhanced/repo/pulls/1.diff", 5 | "patch_url": "https://github.com/reenhanced/repo/pulls/1.patch", 6 | "issue_url": "https://github.com/reenhanced/repo/issue/1", 7 | "number": 1, 8 | "state": "open", 9 | "title": "new-feature", 10 | "body": "Please pull these awesome changes", 11 | "created_at": "2011-01-26T19:01:12Z", 12 | "updated_at": "2011-01-26T19:01:12Z", 13 | "closed_at": "2011-01-26T19:01:12Z", 14 | "merged_at": "2011-01-26T19:01:12Z", 15 | "_links": { 16 | "self": { 17 | "href": "https://api.github.com/reenhanced/repo/pulls/1" 18 | }, 19 | "html": { 20 | "href": "https://github.com/reenhanced/repo/pull/1" 21 | }, 22 | "comments": { 23 | "href": "https://api.github.com/reenhanced/repo/issues/1/comments" 24 | }, 25 | "review_comments": { 26 | "href": "https://api.github.com/reenhanced/repo/pulls/1/comments" 27 | } 28 | }, 29 | "user": { 30 | "login": "reenhanced", 31 | "id": 1, 32 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 33 | "gravatar_id": "", 34 | "url": "https://api.github.com/users/reenhanced", 35 | "html_url": "https://github.com/reenhanced", 36 | "followers_url": "https://api.github.com/users/reenhanced/followers", 37 | "following_url": "https://api.github.com/users/reenhanced/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 41 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 42 | "repos_url": "https://api.github.com/users/reenhanced/repos", 43 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/reenhanced/received_events", 45 | "type": "User", 46 | "site_admin": false 47 | }, 48 | "merged": false, 49 | "mergeable": true, 50 | "merged_by": { 51 | "login": "reenhanced", 52 | "id": 1, 53 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 54 | "gravatar_id": "somehexcode", 55 | "url": "https://api.github.com/users/reenhanced" 56 | }, 57 | "comments": 10, 58 | "commits": 3, 59 | "additions": 100, 60 | "deletions": 3, 61 | "changed_files": 5, 62 | "head": { 63 | "label": "new-feature", 64 | "ref": "new-feature", 65 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 66 | "user": { 67 | "login": "reenhanced", 68 | "id": 1, 69 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 70 | "gravatar_id": "somehexcode", 71 | "url": "https://api.github.com/users/reenhanced" 72 | }, 73 | "repo": { 74 | "url": "https://api.github.com/repos/reenhanced/repo", 75 | "html_url": "https://github.com/reenhanced/repo", 76 | "clone_url": "https://github.com/reenhanced/repo.git", 77 | "git_url": "git://github.com/reenhanced/repo.git", 78 | "ssh_url": "git@github.com:reenhanced/repo.git", 79 | "svn_url": "https://svn.github.com/reenhanced/repo", 80 | "owner": { 81 | "login": "reenhanced", 82 | "id": 1, 83 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 84 | "gravatar_id": "somehexcode", 85 | "url": "https://api.github.com/users/reenhanced" 86 | }, 87 | "name": "repo", 88 | "description": "This your first repo!", 89 | "homepage": "https://github.com", 90 | "language": null, 91 | "private": false, 92 | "fork": false, 93 | "forks": 9, 94 | "watchers": 80, 95 | "size": 108, 96 | "master_branch": "master", 97 | "open_issues": 0, 98 | "pushed_at": "2011-01-26T19:06:43Z", 99 | "created_at": "2011-01-26T19:01:12Z" 100 | } 101 | }, 102 | "base": { 103 | "label": "master", 104 | "ref": "master", 105 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 106 | "user": { 107 | "login": "reenhanced", 108 | "id": 1, 109 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 110 | "gravatar_id": "somehexcode", 111 | "url": "https://api.github.com/users/reenhanced" 112 | }, 113 | "repo": { 114 | "url": "https://api.github.com/repos/reenhanced/repo", 115 | "html_url": "https://github.com/reenhanced/repo", 116 | "clone_url": "https://github.com/reenhanced/repo.git", 117 | "git_url": "git://github.com/reenhanced/repo.git", 118 | "ssh_url": "git@github.com:reenhanced/repo.git", 119 | "svn_url": "https://svn.github.com/reenhanced/repo", 120 | "owner": { 121 | "login": "reenhanced", 122 | "id": 1, 123 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 124 | "gravatar_id": "somehexcode", 125 | "url": "https://api.github.com/users/reenhanced" 126 | }, 127 | "name": "repo", 128 | "description": "This your first repo!", 129 | "homepage": "https://github.com", 130 | "language": null, 131 | "private": false, 132 | "fork": false, 133 | "forks": 9, 134 | "watchers": 80, 135 | "size": 108, 136 | "master_branch": "master", 137 | "open_issues": 0, 138 | "pushed_at": "2011-01-26T19:06:43Z", 139 | "created_at": "2011-01-26T19:01:12Z" 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/pull_request.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>", 3 | "html_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>", 4 | "diff_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>.diff", 5 | "patch_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>.patch", 6 | "issue_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>/issue/<%= number %>", 7 | "number": <%= number %>, 8 | "state": "<%= state %>", 9 | "title": "<%= title %>", 10 | "body": "<%= body %>", 11 | "created_at": "2011-01-26T19:01:12Z", 12 | "updated_at": "2011-01-26T19:01:12Z", 13 | "closed_at": "2011-01-26T19:01:12Z", 14 | "merged_at": "2011-01-26T19:01:12Z", 15 | "_links": { 16 | "self": { 17 | "href": "https://api.github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>" 18 | }, 19 | "html": { 20 | "href": "https://github.com/<%= repo_owner %>/<%= repo_name %>/pull/<%= number %>" 21 | }, 22 | "comments": { 23 | "href": "https://api.github.com/<%= repo_owner %>/<%= repo_name %>/issues/<%= number %>/comments" 24 | }, 25 | "review_comments": { 26 | "href": "https://api.github.com/<%= repo_owner %>/<%= repo_name %>/pulls/<%= number %>/comments" 27 | } 28 | }, 29 | "merged": false, 30 | "mergeable": true, 31 | "merged_by": { 32 | "login": "reenhanced", 33 | "id": 1, 34 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 35 | "gravatar_id": "somehexcode", 36 | "url": "https://api.github.com/users/reenhanced" 37 | }, 38 | "comments": 10, 39 | "commits": 3, 40 | "additions": 100, 41 | "deletions": 3, 42 | "changed_files": 5, 43 | "user": { 44 | "login": "<%= owner %>", 45 | "id": 1, 46 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 47 | "gravatar_id": "", 48 | "url": "https://api.github.com/users/<%= owner %>", 49 | "html_url": "https://github.com/<%= owner %>", 50 | "followers_url": "https://api.github.com/users/<%= owner %>/followers", 51 | "following_url": "https://api.github.com/users/<%= owner %>/following{/other_user}", 52 | "gists_url": "https://api.github.com/users/<%= owner %>/gists{/gist_id}", 53 | "starred_url": "https://api.github.com/users/<%= owner %>/starred{/owner}{/repo}", 54 | "subscriptions_url": "https://api.github.com/users/<%= owner %>/subscriptions", 55 | "organizations_url": "https://api.github.com/users/<%= owner %>/orgs", 56 | "repos_url": "https://api.github.com/users/<%= owner %>/repos", 57 | "events_url": "https://api.github.com/users/<%= owner %>/events{/privacy}", 58 | "received_events_url": "https://api.github.com/users/<%= owner %>/received_events", 59 | "type": "User", 60 | "site_admin": false 61 | }, 62 | "head": { 63 | "label": "<%= feature_branch %>", 64 | "ref": "<%= feature_branch %>", 65 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 66 | "user": { 67 | "login": "<%= owner %>", 68 | "id": 1, 69 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 70 | "gravatar_id": "somehexcode", 71 | "url": "https://api.github.com/users/<%= owner %>" 72 | }, 73 | "repo": { 74 | "url": "https://api.github.com/repos/<%= feature_repo_owner %>/<%= repo_name %>", 75 | "html_url": "https://github.com/<%= feature_repo_owner %>/<%= repo_name %>", 76 | "clone_url": "https://github.com/<%= feature_repo_owner %>/<%= repo_name %>.git", 77 | "git_url": "git://github.com/<%= feature_repo_owner %>/<%= repo_name %>.git", 78 | "ssh_url": "git@github.com:<%= feature_repo_owner %>/<%= repo_name %>.git", 79 | "svn_url": "https://svn.github.com/<%= feature_repo_owner %>/<%= repo_name %>", 80 | "owner": { 81 | "login": "<%= owner %>", 82 | "id": 1, 83 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 84 | "gravatar_id": "somehexcode", 85 | "url": "https://api.github.com/users/<%= owner %>" 86 | }, 87 | "name": "<%= repo_name %>", 88 | "description": "This your first repo!", 89 | "homepage": "https://github.com", 90 | "language": null, 91 | "private": false, 92 | "fork": false, 93 | "forks": 9, 94 | "watchers": 80, 95 | "size": 108, 96 | "master_branch": "master", 97 | "open_issues": 1, 98 | "pushed_at": "2011-01-26T19:06:43Z", 99 | "created_at": "2011-01-26T19:01:12Z" 100 | } 101 | }, 102 | "base": { 103 | "label": "<%= base_branch %>", 104 | "ref": "<%= base_branch %>", 105 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 106 | "user": { 107 | "login": "reenhanced", 108 | "id": 1, 109 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 110 | "gravatar_id": "somehexcode", 111 | "url": "https://api.github.com/users/reenhanced" 112 | }, 113 | "repo": { 114 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>", 115 | "html_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>", 116 | "clone_url": "https://github.com/<%= repo_owner %>/<%= repo_name %>.git", 117 | "git_url": "git://github.com/<%= repo_owner %>/<%= repo_name %>.git", 118 | "ssh_url": "git@github.com:<%= repo_owner %>/<%= repo_name %>.git", 119 | "svn_url": "https://svn.github.com/<%= repo_owner %>/<%= repo_name %>", 120 | "owner": { 121 | "login": "reenhanced", 122 | "id": 1, 123 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 124 | "gravatar_id": "somehexcode", 125 | "url": "https://api.github.com/users/reenhanced" 126 | }, 127 | "name": "repo", 128 | "description": "This your first repo!", 129 | "homepage": "https://github.com", 130 | "language": null, 131 | "private": false, 132 | "fork": false, 133 | "forks": 9, 134 | "watchers": 80, 135 | "size": 108, 136 | "master_branch": "master", 137 | "open_issues": 1, 138 | "pushed_at": "2011-01-26T19:06:43Z", 139 | "created_at": "2011-01-26T19:01:12Z" 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/pull_request_branch_nonexistent_error.json: -------------------------------------------------------------------------------- 1 | { 2 | :method => :post, 3 | :body => "{ 4 | \"errors\" : [{ 5 | \"code\" : \"invalid\", 6 | \"field\" : \"head\", 7 | \"message\" : \"head The branch reenhanced:banana does not exist.\", 8 | \"resource\" : \"PullRequest\" }], 9 | \"message\" : \"Validation Failed\"}", 10 | :url => "https://api.github.com/repos/reenhanced/gitreflow/pulls?access_token=12345", 11 | :request_headers => { 12 | "Content-Type" => "application/json", 13 | "Authorization" => "Token token=\"12345\"" 14 | }, 15 | :parallel_manager => nil, 16 | :request => {:proxy => nil}, 17 | :ssl => {}, 18 | :status => 422, 19 | :response_headers => { 20 | "server" => "nginx/1.0.13", 21 | "date" => "Fri, 27 Apr 2012 13:02:49 GMT", 22 | "content-type" => "application/json; charset=utf-8", 23 | "connection" => "close", 24 | "status" => "422 Unprocessable Entity", 25 | "x-ratelimit-limit" => "5000", 26 | "etag" => "\"ebdeb717fe19444c308e608728569d5a\"", 27 | "x-oauth-scopes" => "repo", 28 | "x-ratelimit-remaining" => "4996", 29 | "x-accepted-oauth-scopes" => "repo", 30 | "content-length" => "192"}, 31 | :response => "" 32 | } 33 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/pull_request_exists_error.json: -------------------------------------------------------------------------------- 1 | { 2 | :method => :post, 3 | :body => "{ 4 | \"errors\" : [{ 5 | \"code\" : \"custom\", 6 | \"field\" : \"base\", 7 | \"message\" : \"base A pull request already exists for reenhanced:banana.\", 8 | \"resource\" : \"PullRequest\" }], 9 | \"message\" : \"Validation Failed\"}", 10 | :url => "https://api.github.com/repos/reenhanced/gitreflow/pulls?access_token=12345", 11 | :request_headers => { 12 | "Content-Type" => "application/json", 13 | "Authorization" => "Token token=\"12345\"" 14 | }, 15 | :parallel_manager => nil, 16 | :request => {:proxy => nil}, 17 | :ssl => {}, 18 | :status => 422, 19 | :response_headers => { 20 | "server" => "nginx/1.0.13", 21 | "date" => "Fri, 27 Apr 2012 13:02:49 GMT", 22 | "content-type" => "application/json; charset=utf-8", 23 | "connection" => "close", 24 | "status" => "422 Unprocessable Entity", 25 | "x-ratelimit-limit" => "5000", 26 | "etag" => "\"ebdeb717fe19444c308e608728569d5a\"", 27 | "x-oauth-scopes" => "repo", 28 | "x-ratelimit-remaining" => "4996", 29 | "x-accepted-oauth-scopes" => "repo", 30 | "content-length" => "192"}, 31 | :response => "" 32 | } 33 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/pull_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/reenhanced/repo/pulls/1", 4 | "html_url": "https://github.com/reenhanced/repo/pulls/1", 5 | "diff_url": "https://github.com/reenhanced/repo/pulls/1.diff", 6 | "patch_url": "https://github.com/reenhanced/repo/pulls/1.patch", 7 | "issue_url": "https://github.com/reenhanced/repo/issue/1", 8 | "number": 1, 9 | "state": "open", 10 | "title": "new-feature", 11 | "body": "Please pull these awesome changes", 12 | "created_at": "2011-01-26T19:01:12Z", 13 | "updated_at": "2011-01-26T19:01:12Z", 14 | "closed_at": "2011-01-26T19:01:12Z", 15 | "merged_at": "2011-01-26T19:01:12Z", 16 | "user": { 17 | "login": "reenhanced", 18 | "id": 1, 19 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 20 | "gravatar_id": "", 21 | "url": "https://api.github.com/users/reenhanced", 22 | "html_url": "https://github.com/reenhanced", 23 | "followers_url": "https://api.github.com/users/reenhanced/followers", 24 | "following_url": "https://api.github.com/users/reenhanced/following{/other_user}", 25 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 26 | "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 27 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 28 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 29 | "repos_url": "https://api.github.com/users/reenhanced/repos", 30 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 31 | "received_events_url": "https://api.github.com/users/reenhanced/received_events", 32 | "type": "User", 33 | "site_admin": false 34 | }, 35 | "head": { 36 | "label": "new-feature", 37 | "ref": "new-feature", 38 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 39 | "user": { 40 | "login": "reenhanced", 41 | "id": 1, 42 | "avatar_url": "https://github.com/images/error/reenhanced_happy.gif", 43 | "gravatar_id": "somehexcode", 44 | "url": "https://api.github.com/users/reenhanced" 45 | }, 46 | "repo": { 47 | "url": "https://api.github.com/repos/reenhanced/repo", 48 | "html_url": "https://github.com/reenhanced/repo", 49 | "clone_url": "https://github.com/reenhanced/repo.git", 50 | "git_url": "git://github.com/reenhanced/repo.git", 51 | "ssh_url": "git@github.com:reenhanced/repo.git", 52 | "svn_url": "https://svn.github.com/reenhanced/repo", 53 | "mirror_url": "git://git.example.com/reenhanced/repo", 54 | "id": 1296269, 55 | "owner": { 56 | "login": "reenhanced", 57 | "id": 1, 58 | "avatar_url": "https://github.com/images/error/reenhanced_happy.gif", 59 | "gravatar_id": "somehexcode", 60 | "url": "https://api.github.com/users/reenhanced" 61 | }, 62 | "name": "repo", 63 | "description": "This your first repo!", 64 | "homepage": "https://github.com", 65 | "language": null, 66 | "private": false, 67 | "fork": false, 68 | "forks": 9, 69 | "watchers": 80, 70 | "size": 108, 71 | "master_branch": "master", 72 | "open_issues": 0, 73 | "pushed_at": "2011-01-26T19:06:43Z", 74 | "created_at": "2011-01-26T19:01:12Z", 75 | "updated_at": "2011-01-26T19:14:43Z" 76 | } 77 | }, 78 | "base": { 79 | "label": "master", 80 | "ref": "master", 81 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 82 | "user": { 83 | "login": "reenhanced", 84 | "id": 1, 85 | "avatar_url": "https://github.com/images/error/reenhanced_happy.gif", 86 | "gravatar_id": "somehexcode", 87 | "url": "https://api.github.com/users/reenhanced" 88 | }, 89 | "repo": { 90 | "url": "https://api.github.com/repos/reenhanced/repo", 91 | "html_url": "https://github.com/reenhanced/repo", 92 | "clone_url": "https://github.com/reenhanced/repo.git", 93 | "git_url": "git://github.com/reenhanced/repo.git", 94 | "ssh_url": "git@github.com:reenhanced/repo.git", 95 | "svn_url": "https://svn.github.com/reenhanced/repo", 96 | "mirror_url": "git://git.example.com/reenhanced/repo", 97 | "id": 1296269, 98 | "owner": { 99 | "login": "reenhanced", 100 | "id": 1, 101 | "avatar_url": "https://github.com/images/error/reenhanced_happy.gif", 102 | "gravatar_id": "somehexcode", 103 | "url": "https://api.github.com/users/reenhanced" 104 | }, 105 | "name": "repo", 106 | "description": "This your first repo!", 107 | "homepage": "https://github.com", 108 | "language": null, 109 | "private": false, 110 | "fork": false, 111 | "forks": 9, 112 | "watchers": 80, 113 | "size": 108, 114 | "master_branch": "master", 115 | "open_issues": 0, 116 | "pushed_at": "2011-01-26T19:06:43Z", 117 | "created_at": "2011-01-26T19:01:12Z", 118 | "updated_at": "2011-01-26T19:14:43Z" 119 | } 120 | }, 121 | "_links": { 122 | "self": { 123 | "href": "https://api.github.com/reenhanced/repo/pulls/1" 124 | }, 125 | "html": { 126 | "href": "https://github.com/reenhanced/repo/pull/1" 127 | }, 128 | "comments": { 129 | "href": "https://api.github.com/reenhanced/repo/issues/1/comments" 130 | }, 131 | "review_comments": { 132 | "href": "https://api.github.com/reenhanced/repo/pulls/1/comments" 133 | } 134 | } 135 | } 136 | ] 137 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/review.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "id": <%= id || 1 %>, 3 | "user": { 4 | "login": "<%= author %>", 5 | "id": 1, 6 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 7 | "gravatar_id": "", 8 | "url": "https://api.github.com/users/<%= author %>", 9 | "html_url": "https://github.com/octocat", 10 | "followers_url": "https://api.github.com/users/<%= author %>/followers", 11 | "following_url": "https://api.github.com/users/<%= author %>/following{/other_user}", 12 | "gists_url": "https://api.github.com/users/<%= author %>/gists{/gist_id}", 13 | "starred_url": "https://api.github.com/users/<%= author %>/starred{/owner}{/repo}", 14 | "subscriptions_url": "https://api.github.com/users/<%= author %>/subscriptions", 15 | "organizations_url": "https://api.github.com/users/<%= author %>/orgs", 16 | "repos_url": "https://api.github.com/users/<%= author %>/repos", 17 | "events_url": "https://api.github.com/users/<%= author %>/events{/privacy}", 18 | "received_events_url": "https://api.github.com/users/<%= author %>/received_events", 19 | "type": "User", 20 | "site_admin": false 21 | }, 22 | "body": "<%= body || "Nice." %>", 23 | "commit_id": "<%= commit_id || "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091"%>", 24 | "state": "<%= state || "PENDING" %>", 25 | "html_url": "https://github.com/Hello-World/pull/<%= pull_request_number %>#pullrequestreview-<%= id || 1 %>", 26 | "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/<%= pull_request_number %>", 27 | "_links": { 28 | "html": { 29 | "href": "https://github.com/octocat/Hello-World/pull/<%= pull_request_number %>#pullrequestreview-<%= id || 1 %>" 30 | }, 31 | "pull_request": { 32 | "href": "https://api.github.com/repos/octocat/Hello-World/pulls/<%= pull_request_number %>" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/pull_requests/reviews.json.erb: -------------------------------------------------------------------------------- 1 | [ 2 | <% reviews_json = [] %> 3 | <% reviews.each_with_index do |review, index| %> 4 | <% reviews_json << Fixture.new('pull_requests/review.json.erb', 5 | id: index, 6 | author: review[:author], 7 | pull_request_number: pull_request_number, 8 | repo_owner: repo_owner, 9 | repo_name: repo_name, 10 | body: review[:body] || 'Hammer time', 11 | state: review[:state] || 'PENDING', 12 | commit_id: review[:commit_id] || 'ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091' 13 | ).to_s %> 14 | <% end %> 15 | <%= reviews_json.join(", ") %> 16 | ] 17 | -------------------------------------------------------------------------------- /spec/fixtures/repositories/commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 3 | "commit": { 4 | "url": "https://api.github.com/repos/reenhanced/repo/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", 5 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 6 | "author": { 7 | "name": "Monalisa Octocat", 8 | "email": "support@github.com", 9 | "date": "2011-04-14T16:00:49Z" 10 | }, 11 | "committer": { 12 | "name": "Monalisa Octocat", 13 | "email": "support@github.com", 14 | "date": "2011-04-14T16:00:49Z" 15 | }, 16 | "message": "Fix all the bugs", 17 | "tree": { 18 | "url": "https://api.github.com/repos/reenhanced/repo/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", 19 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" 20 | } 21 | }, 22 | "author": { 23 | "login": "reenhanced", 24 | "id": 1, 25 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 26 | "url": "https://api.github.com/users/reenhanced" 27 | }, 28 | "committer": { 29 | "login": "reenhanced", 30 | "id": 1, 31 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 32 | "url": "https://api.github.com/users/reenhanced" 33 | }, 34 | "parents": [ 35 | { 36 | "url": "https://api.github.com/repos/reenhanced/repo/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", 37 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" 38 | } 39 | ], 40 | "stats": { 41 | "additions": 104, 42 | "deletions": 4, 43 | "total": 108 44 | }, 45 | "files": [ 46 | { 47 | "filename": "file1.txt", 48 | "additions": 10, 49 | "deletions": 2, 50 | "total": 12 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /spec/fixtures/repositories/commit.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 3 | "commit": { 4 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", 5 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 6 | "author": { 7 | "name": "Monalisa Octocat", 8 | "email": "support@github.com", 9 | "date": "<%= created_at || "2011-04-14T16:00:49Z" %>" 10 | }, 11 | "committer": { 12 | "name": "Monalisa Octocat", 13 | "email": "support@github.com", 14 | "date": "<%= created_at || "2011-04-14T16:00:49Z" %>" 15 | }, 16 | "message": "Fix all the bugs", 17 | "tree": { 18 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", 19 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" 20 | } 21 | }, 22 | "author": { 23 | "login": "<%= author %>", 24 | "id": 1, 25 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 26 | "url": "https://api.github.com/users/<%= author %>" 27 | }, 28 | "committer": { 29 | "login": "<%= author %>", 30 | "id": 1, 31 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 32 | "url": "https://api.github.com/users/<%= author %>" 33 | }, 34 | "parents": [ 35 | { 36 | "url": "https://api.github.com/repos/<%= repo_owner %>/<%= repo_name %>/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", 37 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" 38 | } 39 | ], 40 | "stats": { 41 | "additions": 104, 42 | "deletions": 4, 43 | "total": 108 44 | }, 45 | "files": [ 46 | { 47 | "filename": "file1.txt", 48 | "additions": 10, 49 | "deletions": 2, 50 | "total": 12 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /spec/fixtures/repositories/commits.json.erb: -------------------------------------------------------------------------------- 1 | [ 2 | <% commits_json = [] %> 3 | <% commits.each_with_index do |commit, index| %> 4 | <% commits_json << Fixture.new('repositories/commit.json.erb', 5 | id: index, 6 | author: commit[:author], 7 | repo_owner: repo_owner, 8 | repo_name: repo_name, 9 | created_at: commit[:created_at] || Chronic.parse("October 21, 2015 07:28:00") 10 | ).to_s %> 11 | <% end %> 12 | <%= commits_json.join(", ") %> 13 | ] 14 | -------------------------------------------------------------------------------- /spec/fixtures/repositories/statuses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "created_at": "2012-07-20T01:19:13Z", 4 | "updated_at": "2012-07-20T01:19:13Z", 5 | "state": "success", 6 | "target_url": "https://ci.example.com/1000/output", 7 | "description": "Build has completed successfully", 8 | "id": 1, 9 | "url": "https://api.github.com/repos/reenhanced/example/statuses/1", 10 | "context": "continuous-integration/jenkins", 11 | "creator": { 12 | "login": "reenhanced", 13 | "id": 1, 14 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 15 | "gravatar_id": "somehexcode", 16 | "url": "https://api.github.com/users/reenhanced", 17 | "html_url": "https://github.com/reenhanced", 18 | "followers_url": "https://api.github.com/users/reenhanced/followers", 19 | "following_url": "https://api.github.com/users/reenhanced/following{/other_user}", 20 | "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}", 21 | "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}", 22 | "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions", 23 | "organizations_url": "https://api.github.com/users/reenhanced/orgs", 24 | "repos_url": "https://api.github.com/users/reenhanced/repos", 25 | "events_url": "https://api.github.com/users/reenhanced/events{/privacy}", 26 | "received_events_url": "https://api.github.com/users/reenhanced/received_events", 27 | "type": "User", 28 | "site_admin": false 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /spec/fixtures/users/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "reenhanced", 3 | "id": 1, 4 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 5 | "gravatar_id": "somehexcode", 6 | "url": "https://api.github.com/users/reenhanced", 7 | "name": "monalisa octocat", 8 | "company": "GitHub", 9 | "blog": "https://github.com/blog", 10 | "location": "San Francisco", 11 | "email": "octocat@github.com", 12 | "hireable": false, 13 | "bio": "There once was...", 14 | "public_repos": 2, 15 | "public_gists": 1, 16 | "followers": 20, 17 | "following": 0, 18 | "html_url": "https://github.com/reenhanced", 19 | "created_at": "2008-01-14T04:33:35Z", 20 | "type": "User", 21 | "total_private_repos": 100, 22 | "owned_private_repos": 100, 23 | "private_gists": 81, 24 | "disk_usage": 10000, 25 | "collaborators": 8, 26 | "plan": { 27 | "name": "Medium", 28 | "space": 400, 29 | "collaborators": 10, 30 | "private_repos": 20 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::Config do 4 | describe ".get(key)" do 5 | subject { GitReflow::Config.get('chucknorris.roundhouse') } 6 | it { expect{ subject }.to have_run_command_silently 'git config --get chucknorris.roundhouse', blocking: false } 7 | 8 | context "and getting all values" do 9 | subject { GitReflow::Config.get('chucknorris.roundhouse-kick', all: true) } 10 | it { expect{ subject }.to have_run_command_silently 'git config --get-all chucknorris.roundhouse-kick', blocking: false } 11 | 12 | context "and checking locally only" do 13 | subject { GitReflow::Config.get('chucknorris.jump', local: true) } 14 | it { expect{ subject }.to have_run_command_silently 'git config --local --get chucknorris.jump', blocking: false } 15 | end 16 | end 17 | 18 | context "and checking for updates" do 19 | before { GitReflow::Config.get('chucknorris.roundhouse') } 20 | subject { GitReflow::Config.get('chucknorris.roundhouse') } 21 | it { expect{ subject }.to_not have_run_command_silently 'git config --get chucknorris.roundhouse-kick', blocking: false } 22 | end 23 | 24 | context "and checking locally only" do 25 | subject { GitReflow::Config.get('chucknorris.smash', local: true) } 26 | it { expect{ subject }.to have_run_command_silently 'git config --local --get chucknorris.smash', blocking: false } 27 | end 28 | end 29 | 30 | describe ".set(key)" do 31 | subject { GitReflow::Config.set('chucknorris.roundhouse', 'to the face') } 32 | it { expect{ subject }.to have_run_command_silently "git config -f #{ENV['HOME']}/.gitconfig.reflow --replace-all chucknorris.roundhouse \"to the face\"", blocking: false } 33 | 34 | context "for current project only" do 35 | subject { GitReflow::Config.set('chucknorris.roundhouse', 'to the face', local: true) } 36 | it { expect{ subject }.to have_run_command_silently 'git config --replace-all chucknorris.roundhouse "to the face"', blocking: false } 37 | end 38 | end 39 | 40 | describe ".unset(key)" do 41 | subject { GitReflow::Config.unset('chucknorris.roundhouse') } 42 | it { expect{ subject }.to have_run_command_silently "git config -f #{ENV['HOME']}/.gitconfig.reflow --unset-all chucknorris.roundhouse ", blocking: false } 43 | 44 | context "for multi-value keys" do 45 | subject { GitReflow::Config.unset('chucknorris.roundhouse', value: 'to the face') } 46 | it { expect{ subject }.to have_run_command_silently "git config -f #{ENV['HOME']}/.gitconfig.reflow --unset-all chucknorris.roundhouse \"to the face\"", blocking: false } 47 | end 48 | 49 | context "for current project only" do 50 | subject { GitReflow::Config.unset('chucknorris.roundhouse', local: true) } 51 | it { expect{ subject }.to have_run_command_silently 'git config --unset-all chucknorris.roundhouse ', blocking: false } 52 | 53 | context "for multi-value keys" do 54 | subject { GitReflow::Config.unset('chucknorris.roundhouse', value: 'to the face', local: true) } 55 | it { expect{ subject }.to have_run_command_silently 'git config --unset-all chucknorris.roundhouse "to the face"', blocking: false } 56 | end 57 | end 58 | end 59 | 60 | describe ".add(key)" do 61 | subject { GitReflow::Config.add('chucknorris.roundhouse', 'to the face') } 62 | it { expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --add chucknorris.roundhouse \"to the face\"", blocking: false } 63 | 64 | context "for current project only" do 65 | subject { GitReflow::Config.add('chucknorris.roundhouse', 'to the face', local: true) } 66 | it { expect{ subject }.to have_run_command_silently 'git config --add chucknorris.roundhouse "to the face"', blocking: false } 67 | end 68 | 69 | context "globally" do 70 | subject { GitReflow::Config.add('chucknorris.roundhouse', 'to the face', global: true) } 71 | it { expect{ subject }.to have_run_command_silently 'git config --global --add chucknorris.roundhouse "to the face"', blocking: false } 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/git_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Gitacular 4 | include GitReflow::GitHelpers 5 | extend self 6 | end 7 | 8 | describe GitReflow::GitHelpers do 9 | let(:origin_url) { "git@github.com:reenhanced.spectacular/this-is-the.shit.git" } 10 | 11 | before do 12 | stub_with_fallback(GitReflow::Config, :get).with("remote.origin.url").and_return(origin_url) 13 | 14 | stub_run_for Gitacular 15 | end 16 | 17 | describe ".default_editor" do 18 | subject { Gitacular.default_editor } 19 | 20 | context "when the environment has EDITOR set" do 21 | before { allow(ENV).to receive(:[]).with("EDITOR").and_return("emacs") } 22 | specify { expect(subject).to eql("emacs") } 23 | end 24 | 25 | context "when the environment has no EDITOR set" do 26 | before { allow(ENV).to receive(:[]).with("EDITOR").and_return(nil) } 27 | specify { expect(subject).to eql("vi") } 28 | end 29 | end 30 | 31 | describe ".git_root_dir" do 32 | subject { Gitacular.git_root_dir } 33 | 34 | before { Gitacular.instance_variable_set(:@git_root_dir, nil) } 35 | 36 | it { expect(subject).to eq(Dir.pwd) } 37 | 38 | context "when not in the root directory" do 39 | before { allow(Dir).to receive(:pwd).and_return("/tmp/nope") } 40 | it { expect { subject }.to have_run_command_silently "git rev-parse --show-toplevel" } 41 | end 42 | end 43 | 44 | describe ".git_editor_command" do 45 | subject { Gitacular.git_editor_command } 46 | before { ENV["EDITOR"] = "vim" } 47 | 48 | it "defaults to GitReflow config" do 49 | allow(GitReflow::Config).to receive(:get).with("core.editor").and_return "nano" 50 | 51 | expect(subject).to eq "nano" 52 | end 53 | 54 | it "falls back to the environment variable $EDITOR" do 55 | allow(GitReflow::Config).to receive(:get).with("core.editor").and_return "" 56 | 57 | expect(subject).to eq "vim" 58 | end 59 | end 60 | 61 | describe ".remote_user" do 62 | subject { Gitacular.remote_user } 63 | 64 | it { is_expected.to eq("reenhanced.spectacular") } 65 | 66 | context "remote origin url isn't set" do 67 | let(:origin_url) { "" } 68 | it { is_expected.to eq("") } 69 | end 70 | 71 | context "remote origin uses HTTP" do 72 | let(:origin_url) { "https://github.com/reenhanced.spectacular/this-is-the.shit.git" } 73 | it { is_expected.to eq("reenhanced.spectacular") } 74 | end 75 | end 76 | 77 | describe ".remote_repo_name" do 78 | subject { Gitacular.remote_repo_name } 79 | 80 | it { is_expected.to eq("this-is-the.shit") } 81 | 82 | context "remote origin url isn't set" do 83 | let(:origin_url) { "" } 84 | it { is_expected.to eq("") } 85 | end 86 | 87 | context "remote origin uses HTTP" do 88 | let(:origin_url) { "https://github.com/reenhanced.spectacular/this-is-the.shit.git" } 89 | it { is_expected.to eq("this-is-the.shit") } 90 | end 91 | end 92 | 93 | describe ".default_base_branch" do 94 | subject { Gitacular.default_base_branch } 95 | it { is_expected.to eq("master") } 96 | 97 | context "when configured" do 98 | before { allow(GitReflow::Config).to receive(:get).with("reflow.base-branch").and_return("tuba") } 99 | it { is_expected.to eq("tuba") } 100 | end 101 | end 102 | 103 | describe ".current_branch" do 104 | subject { Gitacular.current_branch } 105 | it { 106 | expect { subject } 107 | .to have_run_command_silently "git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g'" 108 | } 109 | end 110 | 111 | describe ".pull_request_template" do 112 | subject { Gitacular.pull_request_template } 113 | 114 | context "template file exists" do 115 | let(:root_dir) { "/some_repo" } 116 | let(:template_content) { "Template content" } 117 | 118 | before do 119 | allow(Gitacular).to receive(:git_root_dir).and_return(root_dir) 120 | allow(File).to receive(:exist?).with("#{root_dir}/.github/PULL_REQUEST_TEMPLATE.md").and_return(true) 121 | allow(File).to receive(:read).with("#{root_dir}/.github/PULL_REQUEST_TEMPLATE.md").and_return(template_content) 122 | end 123 | it { is_expected.to eq template_content } 124 | end 125 | 126 | context "template file does not exist" do 127 | before do 128 | allow(File).to receive(:exist?).and_return(false) 129 | end 130 | 131 | it { is_expected.to be_nil } 132 | end 133 | 134 | context "custom template file configured" do 135 | before do 136 | allow(GitReflow::Config).to receive(:get).with("templates.pull-request").and_return "pr_template_file.md" 137 | end 138 | 139 | context "template file exists" do 140 | let(:template_content) { "Template content" } 141 | 142 | before do 143 | allow(File).to receive(:exist?).with("pr_template_file.md").and_return(true) 144 | allow(File).to receive(:read).with("pr_template_file.md").and_return(template_content) 145 | end 146 | it { is_expected.to eq template_content } 147 | end 148 | 149 | context "template file does not exist" do 150 | before do 151 | allow(File).to receive(:exist?).and_return(false) 152 | end 153 | 154 | it { is_expected.to be_nil } 155 | end 156 | end 157 | end 158 | 159 | describe ".merge_commit_template" do 160 | subject { Gitacular.merge_commit_template } 161 | 162 | context "template file exists" do 163 | let(:root_dir) { "/some_repo" } 164 | let(:template_content) { "Template content" } 165 | 166 | before do 167 | allow(Gitacular).to receive(:git_root_dir).and_return(root_dir) 168 | allow(File).to receive(:exist?).with("#{root_dir}/.github/MERGE_COMMIT_TEMPLATE.md").and_return(true) 169 | allow(File).to receive(:read).with("#{root_dir}/.github/MERGE_COMMIT_TEMPLATE.md").and_return(template_content) 170 | end 171 | 172 | it { is_expected.to eq template_content } 173 | 174 | context "when template has mustache tags" do 175 | let(:template_content) { "This is the coolest {{current_branch}}" } 176 | before { allow(GitReflow).to receive(:current_branch).and_return("tomato") } 177 | it { is_expected.to eq "This is the coolest tomato" } 178 | end 179 | end 180 | 181 | context "template file does not exist" do 182 | before do 183 | allow(File).to receive(:exist?).and_return(false) 184 | end 185 | 186 | it { is_expected.to be_nil } 187 | end 188 | 189 | context "custom template file configured" do 190 | before do 191 | allow(GitReflow::Config).to receive(:get).with("templates.merge-commit").and_return "merge_template_file.md" 192 | end 193 | 194 | context "template file exists" do 195 | let(:template_content) { "Template content" } 196 | 197 | before do 198 | allow(File).to receive(:exist?).with("merge_template_file.md").and_return(true) 199 | allow(File).to receive(:read).with("merge_template_file.md").and_return(template_content) 200 | end 201 | it { is_expected.to eq template_content } 202 | end 203 | 204 | context "template file does not exist" do 205 | before do 206 | allow(File).to receive(:exist?).and_return(false) 207 | end 208 | 209 | it { is_expected.to be_nil } 210 | end 211 | end 212 | end 213 | 214 | describe ".get_first_commit_message" do 215 | subject { Gitacular.get_first_commit_message } 216 | it { expect { subject }.to have_run_command_silently 'git log --pretty=format:"%s" --no-merges -n 1' } 217 | end 218 | 219 | describe ".push_current_branch" do 220 | subject { Gitacular.push_current_branch } 221 | before { allow(Gitacular).to receive(:current_branch).and_return("bingo") } 222 | it { expect { subject }.to have_run_command "git push origin bingo" } 223 | end 224 | 225 | describe ".fetch_destination(destination_branch)" do 226 | subject { Gitacular.fetch_destination("new-feature") } 227 | it { expect { subject }.to have_run_command "git fetch origin new-feature" } 228 | end 229 | 230 | describe ".update_destination(destination_branch)" do 231 | let(:current_branch) { "bananas" } 232 | let(:destination_branch) { "monkey-business" } 233 | 234 | before { allow(Gitacular).to receive(:current_branch).and_return(current_branch) } 235 | subject { Gitacular.update_destination(destination_branch) } 236 | 237 | it "updates the destination branch with the latest code from the remote repo" do 238 | expect { subject }.to have_run_commands_in_order [ 239 | "git checkout #{destination_branch}", 240 | "git pull origin #{destination_branch}", 241 | "git checkout #{current_branch}" 242 | ] 243 | end 244 | end 245 | 246 | describe ".update_current_branch" do 247 | subject { Gitacular.update_current_branch } 248 | before { allow(Gitacular).to receive(:current_branch).and_return("new-feature") } 249 | 250 | it "updates the remote changes and pushes any local changes" do 251 | expect { subject }.to have_run_commands_in_order [ 252 | "git pull origin new-feature", 253 | "git push origin new-feature" 254 | ] 255 | end 256 | end 257 | 258 | describe ".update_feature_branch" do 259 | options = { base: "base", remote: "remote" } 260 | subject { Gitacular.update_feature_branch(options) } 261 | before { allow(Gitacular).to receive(:current_branch).and_return("feature") } 262 | 263 | it "calls the correct methods" do 264 | expect { subject }.to have_run_commands_in_order [ 265 | "git checkout base", 266 | "git pull remote base", 267 | "git checkout feature", 268 | "git pull origin feature", 269 | "git merge base" 270 | ] 271 | end 272 | end 273 | 274 | describe ".append_to_merge_commit_message(message)" do 275 | let(:original_commit_message) { "Oooooo, SQUASH IT" } 276 | let(:message) { "do do the voodoo that you do" } 277 | let(:root_dir) { "/home/gitreflow" } 278 | let(:merge_message_path) { "#{root_dir}/.git/SQUASH_MSG" } 279 | let(:tmp_merge_message_path) { "#{root_dir}/.git/tmp_merge_msg" } 280 | before { allow(Gitacular).to receive(:git_root_dir).and_return(root_dir) } 281 | subject { Gitacular.append_to_merge_commit_message(message) } 282 | 283 | it "appends the message to git's SQUASH_MSG temp file" do 284 | tmp_file = double("file") 285 | allow(File).to receive(:open).with(tmp_merge_message_path, "w").and_yield(tmp_file) 286 | allow(File).to receive(:exists?).with(merge_message_path).and_return(true) 287 | allow(File).to receive(:foreach).with(merge_message_path).and_yield(original_commit_message) 288 | expect(tmp_file).to receive(:puts).with(message) 289 | expect(tmp_file).to receive(:puts).with(original_commit_message) 290 | 291 | expect { subject }.to have_run_commands_in_order [ 292 | "mv #{tmp_merge_message_path} #{merge_message_path}" 293 | ] 294 | end 295 | 296 | context "when doing a direct merge" do 297 | let(:merge_message_path) { "#{root_dir}/.git/MERGE_MSG" } 298 | subject { Gitacular.append_to_merge_commit_message(message, merge_method: "merge") } 299 | it "appends the message to git's MERGE_MSG temp file if using a direct merge" do 300 | tmp_file = double("file") 301 | allow(File).to receive(:open).with(tmp_merge_message_path, "w").and_yield(tmp_file) 302 | allow(File).to receive(:exists?).with(merge_message_path).and_return(true) 303 | allow(File).to receive(:foreach).with(merge_message_path).and_yield(original_commit_message) 304 | expect(tmp_file).to receive(:puts).with(message) 305 | expect(tmp_file).to receive(:puts).with(original_commit_message) 306 | 307 | expect { subject }.to have_run_commands_in_order [ 308 | "mv #{tmp_merge_message_path} #{merge_message_path}" 309 | ] 310 | end 311 | end 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/git_server/bit_bucket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::GitServer::BitBucket do 4 | let(:user) { 'reenhanced' } 5 | let(:password) { 'shazam' } 6 | let(:repo) { 'repo' } 7 | let(:api_key) { 'a1b2c3d4e5f6g7h8i9j0' } 8 | let(:hostname) { 'hostname.local' } 9 | let(:api_endpoint) { 'https://bitbucket.org/api/1.0' } 10 | let(:site) { 'https://bitbucket.org' } 11 | let(:remote_url) { "git@bitbucket.org:#{user}/#{repo}.git" } 12 | 13 | before do 14 | allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question| 15 | values = { 16 | "Please enter your BitBucket username: " => user 17 | } 18 | return_value = values[question] 19 | question = "" 20 | return_value 21 | end 22 | end 23 | 24 | describe '#initialize(options)' do 25 | subject { GitReflow::GitServer::BitBucket.new({}) } 26 | 27 | it 'sets the reflow git server provider to BitBucket in the git config' do 28 | expect(GitReflow::Config).to receive(:set).once.with('reflow.git-server', 'BitBucket', local: false) 29 | subject 30 | end 31 | 32 | context 'storing git config settings only for this project' do 33 | subject { GitReflow::GitServer::BitBucket.new(project_only: true) } 34 | 35 | it 'sets the enterprise site and api as the site and api endpoints for the BitBucket provider in the git config' do 36 | expect(GitReflow::Config).to receive(:set).once.with('reflow.git-server', 'BitBucket', local: true) 37 | subject 38 | end 39 | end 40 | 41 | end 42 | 43 | describe '#authenticate' do 44 | let(:bitbucket) { GitReflow::GitServer::BitBucket.new( { }) } 45 | let!(:bitbucket_api) { BitBucket.new } 46 | subject { bitbucket.authenticate } 47 | 48 | context 'already authenticated' do 49 | it "notifies the user of successful setup" do 50 | allow(GitReflow::Config).to receive(:set).with('reflow.git-server', 'BitBucket', local: false) 51 | allow(GitReflow::Config).to receive(:get).with('remote.origin.url').and_return(remote_url) 52 | allow(GitReflow::Config).to receive(:get).with('bitbucket.user', local: false).and_return(user) 53 | allow(GitReflow::Config).to receive(:get).with('bitbucket.api-key', reload: true, local: false).and_return(api_key) 54 | allow(GitReflow::Config).to receive(:get).with('reflow.local-projects', all: true).and_return('') 55 | expect { subject }.to have_said "\nYour BitBucket account was already setup with:" 56 | expect { subject }.to have_said "\tUser Name: #{user}" 57 | end 58 | end 59 | 60 | context 'not yet authenticated' do 61 | context 'with valid BitBucket credentials' do 62 | before do 63 | allow(GitReflow::Config).to receive(:get).and_return('') 64 | allow(GitReflow::Config).to receive(:set) 65 | allow(GitReflow::Config).to receive(:set).with('bitbucket.api-key', reload: true).and_return(api_key) 66 | allow(GitReflow::Config).to receive(:get).with('bitbucket.api-key', reload: true).and_return('') 67 | allow(GitReflow::Config).to receive(:get).with('remote.origin.url').and_return(remote_url) 68 | allow(GitReflow::Config).to receive(:get).with('reflow.local-projects').and_return('') 69 | allow(bitbucket).to receive(:connection).and_return double(repos: double(all: [])) 70 | end 71 | 72 | it "prompts me to setup an API key" do 73 | expect { subject }.to have_said "\nIn order to connect your BitBucket account," 74 | expect { subject }.to have_said "you'll need to generate an API key for your team" 75 | expect { subject }.to have_said "Visit https://bitbucket.org/account/user/reenhanced/api-key/, to generate it\n" 76 | end 77 | end 78 | end 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/git_server/git_hub_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::GitServer::GitHub do 4 | let(:user) { 'reenhanced' } 5 | let(:password) { 'shazam' } 6 | let(:repo) { 'repo' } 7 | let(:oauth_token_hash) { Hashie::Mash.new({ token: 'a1b2c3d4e5f6g7h8i9j0'}) } 8 | let(:hostname) { 'hostname.local' } 9 | let(:github_site) { 'https://github.com' } 10 | let(:github_api_endpoint) { 'https://api.github.com' } 11 | let(:enterprise_site) { 'https://github.gittyup.com' } 12 | let(:enterprise_api) { 'https://github.gittyup.com/api/v3' } 13 | let(:github) { stub_github_with(pull: existing_pull_request) } 14 | let!(:github_api) { github.connection } 15 | let(:existing_pull_request) { Fixture.new('pull_requests/pull_request.json').to_json_hashie } 16 | let(:existing_pull_requests) { Fixture.new('pull_requests/pull_requests.json').to_json_hashie } 17 | 18 | before do 19 | allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question| 20 | values = { 21 | "Please enter your GitHub username: " => user, 22 | "Please enter your GitHub password (we do NOT store this): " => password, 23 | "Please enter your Enterprise site URL (e.g. https://github.company.com):" => enterprise_site, 24 | "Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):" => enterprise_api 25 | } 26 | return_value = values[question] 27 | question = "" 28 | return_value 29 | end 30 | 31 | allow(github.class).to receive(:remote_user).and_return(user) 32 | allow(github.class).to receive(:remote_repo_name).and_return(repo) 33 | end 34 | 35 | describe '#initialize(options)' do 36 | subject { GitReflow::GitServer::GitHub.new({}) } 37 | 38 | it 'sets the reflow git server provider to GitHub in the git config' do 39 | expect(GitReflow::Config).to receive(:set).once.with('github.site', github_site, local: false) 40 | expect(GitReflow::Config).to receive(:set).once.with('github.endpoint', github_api_endpoint, local: false) 41 | expect(GitReflow::Config).to receive(:set).once.with('reflow.git-server', 'GitHub', local: false) 42 | subject 43 | end 44 | 45 | context 'using enterprise' do 46 | subject { GitReflow::GitServer::GitHub.new(enterprise: true) } 47 | 48 | it 'sets the enterprise site and api as the site and api endpoints for the GitHub provider in the git config' do 49 | expect(GitReflow::Config).to receive(:set).once.with('github.site', enterprise_site, local: false) 50 | expect(GitReflow::Config).to receive(:set).once.with('github.endpoint', enterprise_api, local: false) 51 | expect(GitReflow::Config).to receive(:set).once.with('reflow.git-server', 'GitHub', local: false) 52 | subject 53 | end 54 | 55 | end 56 | 57 | context 'storing git config settings only for this project' do 58 | subject { GitReflow::GitServer::GitHub.new(project_only: true) } 59 | 60 | before do 61 | expect(GitReflow::Config).to receive(:get).twice.with('reflow.local-projects', all: true).and_return("#{user}/#{repo}") 62 | end 63 | 64 | it 'sets the enterprise site and api as the site and api endpoints for the GitHub provider in the git config' do 65 | expect(GitReflow::Config).to receive(:set).once.with('github.site', github_site, local: true).and_call_original 66 | expect(GitReflow::Config).to receive(:set).once.with('github.endpoint', github_api_endpoint, local: true) 67 | expect(GitReflow::Config).to receive(:set).once.with('reflow.git-server', 'GitHub', local: true) 68 | subject 69 | end 70 | end 71 | 72 | end 73 | 74 | describe '#authenticate' do 75 | let(:github) { GitReflow::GitServer::GitHub.new({}) } 76 | let!(:github_api) { Github::Client.new } 77 | let(:github_authorizations) { Github::Client::Authorizations.new } 78 | subject { github.authenticate } 79 | 80 | before do 81 | allow(GitReflow::GitServer::GitHub).to receive(:user).and_return(user) 82 | allow(github_api).to receive(:oauth).and_return(github_authorizations) 83 | allow(github_api).to receive_message_chain(:oauth, :all).and_return([]) 84 | allow(github).to receive(:run).with('hostname', loud: false).and_return(hostname) 85 | end 86 | 87 | context 'not yet authenticated' do 88 | context 'with valid GitHub credentials' do 89 | 90 | before do 91 | allow(Github::Client).to receive(:new).and_return(github_api) 92 | allow(github_authorizations).to receive(:authenticated?).and_return(true) 93 | allow(github_api.oauth).to receive(:create).with({ scopes: ['repo'], note: "git-reflow (#{hostname})" }).and_return(oauth_token_hash) 94 | end 95 | 96 | it "notifies the user of successful setup" do 97 | expect { subject }.to have_said "Your GitHub account was successfully setup!", :success 98 | end 99 | 100 | it "creates a new GitHub oauth token" do 101 | expect(github_api.oauth).to receive(:create).and_return(oauth_token_hash) 102 | subject 103 | end 104 | 105 | it "creates git config keys for github connections" do 106 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.site \"#{GitReflow::GitServer::GitHub.site_url}\"", blocking: false 107 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.endpoint \"#{GitReflow::GitServer::GitHub.api_endpoint}\"", blocking: false 108 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.oauth-token \"#{oauth_token_hash[:token]}\"", blocking: false 109 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all reflow.git-server \"GitHub\"", blocking: false 110 | end 111 | 112 | context "exclusive to project" do 113 | let(:github) do 114 | allow(GitReflow::GitServer::GitHub).to receive(:project_only?).and_return(true) 115 | allow(GitReflow::GitServer::GitHub).to receive(:remote_user).and_return(user) 116 | allow(GitReflow::GitServer::GitHub).to receive(:remote_repo_name).and_return(repo) 117 | GitReflow::GitServer::GitHub.new(project_only: true) 118 | end 119 | 120 | it "creates _local_ git config keys for github connections" do 121 | expect{ subject }.to_not have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.site \"#{GitReflow::GitServer::GitHub.site_url}\"", blocking: false 122 | expect{ subject }.to_not have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.endpoint \"#{GitReflow::GitServer::GitHub.api_endpoint}\"", blocking: false 123 | expect{ subject }.to_not have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.oauth-token \"#{oauth_token_hash[:token]}\"", blocking: false 124 | expect{ subject }.to_not have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all reflow.git-server \"GitHub\"", blocking: false 125 | 126 | expect{ subject }.to have_run_command_silently "git config --replace-all github.site \"#{GitReflow::GitServer::GitHub.site_url}\"", blocking: false 127 | expect{ subject }.to have_run_command_silently "git config --replace-all github.endpoint \"#{GitReflow::GitServer::GitHub.api_endpoint}\"", blocking: false 128 | expect{ subject }.to have_run_command_silently "git config --replace-all github.oauth-token \"#{oauth_token_hash[:token]}\"", blocking: false 129 | expect{ subject }.to have_run_command_silently "git config --replace-all reflow.git-server \"GitHub\"", blocking: false 130 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --add reflow.local-projects \"#{user}/#{repo}\"", blocking: false 131 | end 132 | end 133 | 134 | context "use GitHub enterprise account" do 135 | let(:github) { GitReflow::GitServer::GitHub.new(enterprise: true) } 136 | before { allow(GitReflow::GitServer::GitHub).to receive(:@using_enterprise).and_return(true) } 137 | it "creates git config keys for github connections" do 138 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.site \"#{enterprise_site}\"", blocking: false 139 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.endpoint \"#{enterprise_api}\"", blocking: false 140 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.oauth-token \"#{oauth_token_hash[:token]}\"", blocking: false 141 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all reflow.git-server \"GitHub\"", blocking: false 142 | end 143 | end 144 | end 145 | 146 | context "with invalid GitHub credentials" do 147 | let(:unauthorized_error_response) {{ 148 | response_headers: {'content-type' => 'application/json; charset=utf-8', status: 'Unauthorized'}, 149 | method: 'GET', 150 | status: '401', 151 | body: { error: "GET https://api.github.com/authorizations: 401 Bad credentials" } 152 | }} 153 | 154 | before do 155 | allow(GitReflow::Config).to receive(:get).and_call_original 156 | allow(GitReflow::Config).to receive(:get).with('github.oauth-token').and_return(oauth_token_hash[:token]) 157 | allow(Github::Client).to receive(:new).and_return(github_api) 158 | allow(github_authorizations).to receive(:authenticated?).and_return(true) 159 | allow(github_api.oauth).to receive(:create).with({ scopes: ['repo'], note: "git-reflow (#{hostname})" }).and_return(oauth_token_hash) 160 | 161 | stub_request(:get, %r{/user}). 162 | to_return( 163 | body: Fixture.new('authentication_failure.json').to_s, 164 | status: 401, 165 | headers: {'content-type' => 'application/json; charset=utf-8', status: 'Unauthorized'}, 166 | ) 167 | end 168 | 169 | it "notifies the user of successful setup" do 170 | expect { subject }.to have_said "Your GitHub account was successfully setup!", :success 171 | end 172 | 173 | it "creates a new GitHub oauth token" do 174 | expect(github_api.oauth).to receive(:create).and_return(oauth_token_hash) 175 | subject 176 | end 177 | 178 | it "creates git config keys for github connections" do 179 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.site \"#{github_site}\"", blocking: false 180 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.endpoint \"#{github_api_endpoint}\"", blocking: false 181 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.oauth-token \"#{oauth_token_hash[:token]}\"", blocking: false 182 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all reflow.git-server \"GitHub\"", blocking: false 183 | end 184 | 185 | end 186 | end 187 | 188 | context 'already authenticated' do 189 | let(:oauth_token) { "abc123" } 190 | 191 | before do 192 | allow(GitReflow::Config).to receive(:get).and_call_original 193 | allow(GitReflow::Config).to receive(:get).with('github.oauth-token').and_return(oauth_token) 194 | end 195 | 196 | context "and authentication token is still valid" do 197 | before do 198 | stub_request(:get, %r{/user}). 199 | to_return( 200 | body: Fixture.new('users/user.json').to_s, 201 | status: 200, 202 | headers: { content_type: "application/json; charset=utf-8" } 203 | ) 204 | 205 | allow(Github::Client).to receive(:new).and_return(github_api) 206 | allow(github_api).to receive(:oauth).and_return(github_authorizations) 207 | allow(github_authorizations).to receive(:authenticated?).and_return(true) 208 | allow(github_api.oauth).to receive(:create).with({ scopes: ['repo'], note: "git-reflow (#{hostname})" }).and_return(oauth_token_hash) 209 | end 210 | 211 | it "resolves all missing git-reflow configurations" do 212 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.site \"#{github_site}\"", blocking: false 213 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all github.endpoint \"#{github_api_endpoint}\"", blocking: false 214 | expect{ subject }.to have_run_command_silently "git config -f #{GitReflow::Config::CONFIG_FILE_PATH} --replace-all reflow.git-server \"GitHub\"", blocking: false 215 | expect { subject }.to have_said "Your GitHub account was already setup with: " 216 | expect { subject }.to have_said "\tUser Name: #{user}" 217 | expect { subject }.to have_said "\tEndpoint: #{github_api_endpoint}" 218 | end 219 | end 220 | 221 | context "and authentication token is expired" do 222 | let(:unauthorized_error_response) {{ 223 | response_headers: {'content-type' => 'application/json; charset=utf-8', status: 'Unauthorized'}, 224 | method: 'GET', 225 | status: '401', 226 | body: { error: "GET https://api.github.com/authorizations: 401 Bad credentials" } 227 | }} 228 | 229 | before do 230 | allow(Github::Client).to receive(:new).and_raise Github::Error::Unauthorized.new(unauthorized_error_response) 231 | end 232 | 233 | it "requests a new oauth token" do 234 | end 235 | end 236 | end 237 | end 238 | 239 | describe '#create_pull_request(options)' do 240 | let(:title) { 'Fresh title' } 241 | let(:body) { 'Funky body' } 242 | let(:current_branch) { 'new-feature' } 243 | 244 | subject { github.create_pull_request({ title: title, body: body, base: 'master' }) } 245 | 246 | before do 247 | allow(github.class).to receive(:current_branch).and_return(current_branch) 248 | allow(GitReflow).to receive(:git_server).and_return(github) 249 | stub_request(:post, %r{/repos/#{user}/#{repo}/pulls}). 250 | to_return(body: Fixture.new('pull_requests/pull_request.json').to_s, status: 201, headers: {content_type: "application/json; charset=utf-8"}) 251 | end 252 | 253 | specify { expect(subject.class.to_s).to eq('GitReflow::GitServer::GitHub::PullRequest') } 254 | 255 | it 'creates a pull request using the remote user and repo' do 256 | allow(github_api).to receive(:pull_requests) 257 | expect(github_api.pull_requests).to receive(:create).with(user, repo, title: title, body: body, head: "#{user}:#{current_branch}", base: 'master').and_return(existing_pull_request) 258 | subject 259 | end 260 | end 261 | 262 | describe '#find_open_pull_request(from, to)' do 263 | subject { github.find_open_pull_request({ from: 'new-feature', to: 'master'}) } 264 | 265 | it 'looks for an open pull request matching the remote user/repo' do 266 | expect(subject.number).to eq(existing_pull_requests.first.number) 267 | end 268 | 269 | context 'no pull request exists' do 270 | before do 271 | allow(github_api).to receive(:pull_requests) 272 | expect(github_api.pull_requests).to receive(:all).and_return([]) 273 | end 274 | it { is_expected.to eq(nil) } 275 | end 276 | end 277 | 278 | describe '#get_build_status(sha)' do 279 | let(:sha) { '6dcb09b5b57875f334f61aebed695e2e4193db5e' } 280 | subject { github.get_build_status(sha) } 281 | before { allow(github_api).to receive_message_chain(:repos, :statuses) } 282 | 283 | it 'gets the latest build status for the given commit hash' do 284 | expect(github_api.repos.statuses).to receive(:all).with(user, repo, sha).and_return([{ state: 'success'}]) 285 | subject 286 | end 287 | end 288 | 289 | describe '#comment_authors_for_pull_request(pull_request, options = {})' do 290 | end 291 | 292 | describe '#get_committed_time(commit_sha)' do 293 | end 294 | 295 | end 296 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/git_server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::GitServer do 4 | let(:connection_options) { nil } 5 | 6 | subject { GitReflow::GitServer.connect connection_options } 7 | 8 | before do 9 | allow(GitReflow::GitServer::GitHub).to receive(:new) 10 | 11 | module GitReflow::GitServer 12 | class DummyHub < Base 13 | def initialize(options) 14 | "Initialized with #{options}" 15 | end 16 | 17 | def authenticate(options={}) 18 | end 19 | 20 | def connection 21 | 'Connected!' 22 | end 23 | end 24 | end 25 | end 26 | 27 | describe '.connect(options)' do 28 | it 'initializes a new GitHub server provider by default' do 29 | stubbed_github = Class.new 30 | allow(stubbed_github).to receive(:authenticate) 31 | expect(GitReflow::GitServer::GitHub).to receive(:new).and_return(stubbed_github) 32 | subject 33 | end 34 | 35 | context 'provider is specified' do 36 | let(:connection_options) { {provider: 'DummyHub'}.merge(expected_server_options) } 37 | let(:expected_server_options) {{ basic_auth: 'user:pass', end_point: 'https://api.example.com' }} 38 | 39 | it 'initializes any server provider that has been implemented' do 40 | dummy_hub = GitReflow::GitServer::DummyHub.new({}) 41 | expect(GitReflow::GitServer::DummyHub).to receive(:new).with(expected_server_options).and_return(dummy_hub) 42 | expect(subject).to eq(dummy_hub) 43 | expect($says).not_to include 'GitServer not setup for: DummyHub' 44 | end 45 | end 46 | 47 | context 'provider not yet implemented' do 48 | let(:connection_options) {{ provider: 'GitLab' }} 49 | it { expect{ subject }.to have_said "Error connecting to GitLab: GitServer not setup for \"GitLab\"", :error } 50 | end 51 | end 52 | 53 | describe '.current_provider' do 54 | subject { GitReflow::GitServer.current_provider } 55 | 56 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server', local: true).and_return(nil) } 57 | 58 | context 'Reflow setup to use GitHub' do 59 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('GitHub') } 60 | it { is_expected.to eq(GitReflow::GitServer::GitHub) } 61 | end 62 | 63 | context 'Reflow has not yet been setup' do 64 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('') } 65 | it { is_expected.to be_nil } 66 | it { expect{ subject }.to have_said "Reflow hasn't been setup yet. Run 'git reflow setup' to continue", :notice } 67 | end 68 | 69 | context 'an unknown server provider is stored in the git config' do 70 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('GittyUp') } 71 | 72 | it { is_expected.to be_nil } 73 | it { expect{ subject }.to have_said "GitServer not setup for \"GittyUp\"", :error } 74 | end 75 | end 76 | 77 | describe '.connection' do 78 | subject { GitReflow::GitServer.connection } 79 | 80 | before do 81 | allow(GitReflow::Config).to receive(:get).with('reflow.git-server', local: true).and_return(nil) 82 | allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return(nil) 83 | end 84 | 85 | it { is_expected.to be_nil } 86 | 87 | context "with a valid provider" do 88 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('GitHub') } 89 | it 'calls connection on the provider' do 90 | expect(GitReflow::GitServer::GitHub).to receive(:connection) 91 | subject 92 | end 93 | end 94 | 95 | context "with an invalid provider" do 96 | before { allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('GittyUp') } 97 | it { is_expected.to be_nil } 98 | it { expect{ subject }.to have_said "GitServer not setup for \"GittyUp\"", :error } 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::Logger do 4 | context "defaults" do 5 | it "logs to '/tmp/git-reflow.log' by default" do 6 | logger = described_class.new 7 | expect(logger.instance_variable_get("@logdev").dev.path).to eq GitReflow::Logger::DEFAULT_LOG_FILE 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/sandbox_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe GitReflow::Sandbox do 4 | describe ".run" do 5 | it "is blocking by default when the command exits with a failure" do 6 | allow(GitReflow::Sandbox).to receive(:run).and_call_original 7 | expect { GitReflow::Sandbox.run("ls wtf") }.to raise_error SystemExit, "\"ls wtf\" failed to run." 8 | end 9 | 10 | it "when blocking is flagged off, the command exits silently" do 11 | allow(GitReflow::Sandbox).to receive(:run).and_call_original 12 | expect { GitReflow::Sandbox.run("ls wtf", blocking: false) }.to_not raise_error 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/workflow_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow::Workflow do 4 | 5 | class DummyWorkflow 6 | include GitReflow::Workflow 7 | end 8 | 9 | class DummyBundler 10 | def gemfile(&block) 11 | instance_eval &block 12 | end 13 | 14 | def source(name); end 15 | def gem(name, *args); end 16 | end 17 | 18 | class DummyGemifiedWorkflow 19 | include GitReflow::Workflow 20 | 21 | command :whirl do 22 | puts "whirl" 23 | end 24 | end 25 | 26 | let(:workflow) { DummyWorkflow } 27 | let(:loader) { double() } 28 | 29 | describe ".current" do 30 | subject { GitReflow::Workflow.current } 31 | 32 | before do 33 | allow(GitReflow::Workflows::Core).to receive(:load_raw_workflow) 34 | end 35 | 36 | context "when no workflow is set" do 37 | before { allow(GitReflow::Config).to receive(:get).with("reflow.workflow").and_return('') } 38 | specify { expect( subject ).to eql(GitReflow::Workflows::Core) } 39 | end 40 | 41 | context "when a global workflow is set" do 42 | let(:workflow_path) { File.join(File.expand_path("../../../fixtures", __FILE__), "/awesome_workflow.rb") } 43 | 44 | before { allow(GitReflow::Config).to receive(:get).with("reflow.workflow").and_return(workflow_path) } 45 | specify { expect( subject ).to eql(GitReflow::Workflows::Core) } 46 | end 47 | 48 | context "when a local workflow is set" do 49 | let(:workflow_content) do 50 | <<~WORKFLOW_CONTENT 51 | command :dummy do 52 | GitReflow.say "derp" 53 | end 54 | WORKFLOW_CONTENT 55 | end 56 | 57 | before do 58 | allow(File).to receive(:exists?).with("#{GitReflow.git_root_dir}/Workflow").and_return(true) 59 | allow(File).to receive(:read).with("#{GitReflow.git_root_dir}/Workflow").and_return(workflow_content) 60 | expect(GitReflow::Workflows::Core).to receive(:load_raw_workflow).with(workflow_content).and_call_original 61 | end 62 | 63 | specify { expect( subject ).to respond_to(:dummy) } 64 | specify { expect( subject ).to eql(GitReflow::Workflows::Core) } 65 | end 66 | 67 | context "when both a local and a global workflow are set" do 68 | let(:workflow_path) { File.join(File.expand_path("../../../fixtures", __FILE__), "/awesome_workflow.rb") } 69 | let(:workflow_content) do 70 | <<~WORKFLOW_CONTENT 71 | command :dummy do 72 | GitReflow.say "derp" 73 | end 74 | WORKFLOW_CONTENT 75 | end 76 | 77 | before do 78 | allow(File).to receive(:exists?).with("#{GitReflow.git_root_dir}/Workflow").and_return(true) 79 | allow(File).to receive(:read).with("#{GitReflow.git_root_dir}/Workflow").and_return(workflow_content) 80 | allow(GitReflow::Config).to receive(:get).with("reflow.workflow").and_return(workflow_path) 81 | allow(GitReflow::Workflows::Core).to receive(:load_raw_workflow).and_call_original 82 | end 83 | 84 | specify { expect(subject).to respond_to(:dummy) } 85 | specify { expect(subject).to eql(GitReflow::Workflows::Core) } 86 | end 87 | end 88 | 89 | describe ".before" do 90 | it "executes the block before the command" do 91 | GitReflow::Workflows::Core.load_raw_workflow <<~WORKFLOW_CONTENT 92 | command :yips do 93 | puts "Yips." 94 | end 95 | 96 | before :yips do 97 | puts "Would you like a donut?" 98 | end 99 | WORKFLOW_CONTENT 100 | 101 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(true) 102 | 103 | expect { GitReflow.workflow.yips }.to have_output "Would you like a donut?\nYips." 104 | end 105 | 106 | it "executes blocks sequentially by order of appearance" do 107 | GitReflow::Workflows::Core.load_raw_workflow <<~WORKFLOW_CONTENT 108 | command :yips do 109 | puts "Yips." 110 | end 111 | 112 | before :yips do 113 | puts "Cupcake?" 114 | end 115 | 116 | before :yips do 117 | puts "Would you like a donut?" 118 | end 119 | WORKFLOW_CONTENT 120 | 121 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(true) 122 | 123 | expect { GitReflow.workflow.yips }.to have_output "Cupcake?\nWould you like a donut?\nYips." 124 | end 125 | 126 | it "proxies any arguments returned to the command" do 127 | GitReflow::Workflows::Core.load_raw_workflow <<~WORKFLOW_CONTENT 128 | command :yips, arguments: { spiced: false } do |**params| 129 | puts params[:spiced] ? "Too spicy." : "Yips." 130 | end 131 | 132 | before :yips do 133 | puts "Wasabe?" 134 | { spiced: true } 135 | end 136 | WORKFLOW_CONTENT 137 | 138 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(true) 139 | 140 | expect { GitReflow.workflow.yips }.to have_output "Wasabe?\nToo spicy." 141 | end 142 | end 143 | 144 | describe ".after" do 145 | it "executes the block after the command" do 146 | GitReflow::Workflows::Core.load_raw_workflow <<~WORKFLOW_CONTENT 147 | command :vroom do 148 | puts "Vroom" 149 | end 150 | 151 | after :vroom do 152 | puts "VROOOOM" 153 | end 154 | WORKFLOW_CONTENT 155 | 156 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(true) 157 | 158 | expect { GitReflow.workflow.vroom }.to have_output "Vroom\nVROOOOM" 159 | end 160 | 161 | it "executes blocks sequentially by order of appearance" do 162 | GitReflow::Workflows::Core.load_raw_workflow <<~WORKFLOW_CONTENT 163 | command :vroom do 164 | puts "Vroom" 165 | end 166 | 167 | after :vroom do 168 | puts "Vrooom" 169 | end 170 | 171 | after :vroom do 172 | puts "VROOOOM" 173 | end 174 | WORKFLOW_CONTENT 175 | 176 | allow(GitReflow::Workflows::Core).to receive(:load_workflow).and_return(true) 177 | 178 | expect { GitReflow.workflow.vroom }.to have_output "Vroom\nVrooom\nVROOOOM" 179 | end 180 | end 181 | 182 | describe ".command" do 183 | it "creates a class method for a bogus command" do 184 | workflow.command :bogus do 185 | GitReflow.say "Woohoo" 186 | end 187 | 188 | expect { DummyWorkflow.bogus }.to have_said("Woohoo") 189 | end 190 | 191 | it "creates a method for a bogus command with arguments" do 192 | workflow.command :bogus, arguments: { feature_branch: nil } do |**params| 193 | GitReflow.say "Woohoo #{params[:feature_branch]}!" 194 | end 195 | 196 | expect { DummyWorkflow.bogus(feature_branch: "arguments") }.to have_said("Woohoo arguments!") 197 | end 198 | 199 | it "creates a class method for a bogus command with default arguments" do 200 | workflow.command :bogus, arguments: { feature_branch: nil, decoration: "sprinkles" } do |**params| 201 | donut_excitement = "Woohoo #{params[:feature_branch]}" 202 | donut_excitement += " with #{params[:decoration]}" if params[:decoration] 203 | GitReflow.say "#{donut_excitement}!" 204 | end 205 | 206 | expect { DummyWorkflow.bogus(feature_branch: "donuts") }.to have_said("Woohoo donuts with sprinkles!") 207 | end 208 | 209 | it "creates a class method for a bogus command with flags" do 210 | workflow.command :bogus, flags: { feature_branch: nil } do |**params| 211 | GitReflow.say "Woohoo #{params[:feature_branch]}!" 212 | end 213 | 214 | expect { DummyWorkflow.bogus(feature_branch: "flags") }.to have_said("Woohoo flags!") 215 | end 216 | 217 | it "creates a class method for a bogus command with default flags" do 218 | workflow.command :bogus, flags: { feature_branch: "donuts" } do |**params| 219 | GitReflow.say "Woohoo #{params[:feature_branch]}!" 220 | end 221 | 222 | expect { DummyWorkflow.bogus }.to have_said("Woohoo donuts!") 223 | end 224 | 225 | it "creates a class method for a bogus command with switches" do 226 | workflow.command :bogus, switches: { feature_branch: nil } do |**params| 227 | GitReflow.say "Woohoo #{params[:feature_branch]}!" 228 | end 229 | 230 | expect { DummyWorkflow.bogus(feature_branch: "switches") }.to have_said("Woohoo switches!") 231 | end 232 | 233 | it "creates a class method for a bogus command with default switches" do 234 | workflow.command :bogus, switches: { feature_branch: "donuts" } do |**params| 235 | GitReflow.say "Woohoo #{params[:feature_branch]}!" 236 | end 237 | 238 | expect { DummyWorkflow.bogus }.to have_said("Woohoo donuts!") 239 | end 240 | end 241 | 242 | describe ".use(workflow_name)" do 243 | it "Uses a pre-existing workflow as a basis" do 244 | allow(GitReflow::Workflows::Core).to receive(:load_workflow) 245 | expect(GitReflow::Workflows::Core).to receive(:load_workflow) 246 | .with(workflow.workflows["FlatMergeWorkflow"]) 247 | .and_return(true) 248 | workflow.use "FlatMergeWorkflow" 249 | end 250 | end 251 | 252 | describe ".use_gem(name, *ags)" do 253 | let(:mock_bundler) { DummyBundler.new } 254 | 255 | before do 256 | allow(DummyGemifiedWorkflow).to receive(:gemfile) do |&block| 257 | mock_bundler.gemfile(&block) 258 | end 259 | stub_run_for(DummyGemifiedWorkflow) 260 | end 261 | 262 | it "Installs a gem using Bundler's inline gemfile" do 263 | stub_command(command: "gem list -ie whirly", options: { loud: false, raise: true }, return_value: "false") 264 | expect(mock_bundler).to receive(:source).with("https://rubygems.org") 265 | expect(mock_bundler).to receive(:gem).with("whirly", "0.2.6") 266 | 267 | DummyGemifiedWorkflow.class_eval do 268 | use_gem "whirly", "0.2.6" 269 | end 270 | end 271 | 272 | it "Uses an existing gem if it's already installed" do 273 | stub_command(command: "gem list -ie whirly", options: { loud: false, raise: false }, return_value: "true") 274 | expect(DummyGemifiedWorkflow).to_not receive(:gemfile) 275 | 276 | DummyGemifiedWorkflow.class_eval do 277 | use_gem "whirly", "0.2.6" 278 | end 279 | end 280 | end 281 | 282 | describe ".use_gemfile(&block)" do 283 | let(:mock_bundler) { DummyBundler.new } 284 | 285 | before do 286 | allow(DummyGemifiedWorkflow).to receive(:gemfile) do |&block| 287 | mock_bundler.gemfile(&block) 288 | end 289 | stub_run_for(DummyGemifiedWorkflow) 290 | end 291 | 292 | it "runs bundler's inline gemfile with the provided block" do 293 | expect(mock_bundler).to receive(:source).with("https://rubygems.org") 294 | expect(mock_bundler).to receive(:gem).with("standard") 295 | 296 | DummyGemifiedWorkflow.class_eval do 297 | use_gemfile do 298 | source "https://rubygems.org" 299 | gem "standard" 300 | end 301 | end 302 | end 303 | end 304 | end 305 | -------------------------------------------------------------------------------- /spec/lib/git_reflow/workflows/flat_merge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'FlatMerge' do 4 | let(:mergable_pr) { double(good_to_merge?: true, merge!: true) } 5 | let(:git_server) { double(find_open_pull_request: mergable_pr) } 6 | 7 | before do 8 | allow(GitReflow::Config).to receive(:get).and_call_original 9 | allow(GitReflow).to receive(:git_server).and_return(git_server) 10 | allow(GitReflow).to receive(:status) 11 | # Makes sure we are loading the right workflow 12 | workflow_path = File.join(File.expand_path("../../../../../lib/git_reflow/workflows", __FILE__), "FlatMergeWorkflow") 13 | use_workflow(workflow_path) 14 | end 15 | 16 | after { GitReflow::Workflow.reset! } 17 | 18 | context ".deliver" do 19 | subject { GitReflow.deliver } 20 | 21 | context "with Github" do 22 | let(:github_pr) { Fixture.new('pull_requests/external_pull_request.json').to_json_hashie } 23 | let(:pr) { GitReflow::GitServer::GitHub::PullRequest.new(github_pr) } 24 | let(:github) { stub_github_with } 25 | let!(:github_api) { github.connection } 26 | 27 | before do 28 | allow(File).to receive(:read).and_call_original 29 | allow_any_instance_of(GitReflow::GitServer::PullRequest).to receive(:deliver?).and_return(false) 30 | allow(GitReflow::Workflows::Core).to receive(:status) 31 | allow(GitReflow.git_server).to receive(:get_build_status).and_return(Struct.new(:state, :description, :url, :target_url).new) 32 | allow(GitReflow::GitServer::GitHub::PullRequest).to receive(:find_open).and_return(pr) 33 | allow(pr).to receive(:good_to_merge?).and_return(true) 34 | end 35 | 36 | it "overrides squash merge in favor of flat merge" do 37 | expect(pr).to receive(:merge!).with( 38 | base: "master", 39 | merge_method: "merge", 40 | force: false, 41 | skip_lgtm: false 42 | ) 43 | subject 44 | end 45 | end 46 | 47 | context "when force-merging or with bitbucket" do 48 | let(:pr_response) { Fixture.new('pull_requests/external_pull_request.json').to_json_hashie } 49 | let(:pr) { MockPullRequest.new(pr_response) } 50 | 51 | subject { GitReflow.deliver force: true} 52 | 53 | before do 54 | allow(GitReflow.git_server).to receive(:find_open_pull_request).and_return(pr) 55 | allow(pr).to receive(:good_to_merge?).and_return(true) 56 | allow(GitReflow::Workflows::Core).to receive(:status) 57 | end 58 | 59 | it "doesn't squash merge" do 60 | expect(pr).to receive(:merge!).with( 61 | base: "master", 62 | merge_method: "merge", 63 | force: true, 64 | skip_lgtm: false 65 | ) 66 | subject 67 | end 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/git_reflow_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitReflow do 4 | describe ".logger" do 5 | # Ignore memoization for tests 6 | before { GitReflow.instance_variable_set("@logger", nil) } 7 | 8 | it "initializes a new logger" do 9 | expect(GitReflow::Logger).to receive(:new) 10 | described_class.logger 11 | end 12 | 13 | it "allows for custom loggers" do 14 | logger = described_class.logger("kenny-loggins.log") 15 | expect(logger.instance_variable_get("@logdev").dev.path).to eq "kenny-loggins.log" 16 | end 17 | end 18 | 19 | describe ".git_server" do 20 | subject { GitReflow.git_server } 21 | 22 | before do 23 | allow(GitReflow::Config).to receive(:get) 24 | allow(GitReflow::Config).to receive(:get).with('reflow.git-server').and_return('GitHub ') 25 | end 26 | 27 | it "attempts to connect to the provider" do 28 | expect(GitReflow::GitServer).to receive(:connect).with(provider: 'GitHub', silent: true) 29 | subject 30 | end 31 | end 32 | 33 | context "aliases workflow commands" do 34 | %w{deliver refresh review setup stage start status}.each do |command| 35 | it "aliases the command to the workflow" do 36 | expect( subject.respond_to?(command.to_sym) ).to be_truthy 37 | end 38 | end 39 | 40 | context "when a workflow is set" do 41 | it "calls the defined workflow methods instead of the default core" do 42 | workflow_path = File.join(File.expand_path("../../fixtures", __FILE__), "/awesome_workflow.rb") 43 | allow(GitReflow::Config).to receive(:get).with("reflow.workflow").and_return(workflow_path) 44 | allow(GitReflow::Workflows::Core).to receive(:load_raw_workflow) 45 | expect(GitReflow::Workflows::Core).to receive(:load_raw_workflow).with(File.read(workflow_path)).and_call_original 46 | 47 | expect{ subject.start }.to have_said "Awesome." 48 | end 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'ruby_jard' 4 | require 'multi_json' 5 | require 'webmock/rspec' 6 | 7 | $LOAD_PATH << 'lib' 8 | require 'git_reflow' 9 | 10 | require 'git_reflow/rspec' 11 | 12 | Dir[File.expand_path('../support/**/*.rb', __FILE__)].each {|f| require f} 13 | 14 | RSpec.configure do |config| 15 | config.include WebMock::API 16 | config.include GitReflow::RSpec::CommandLineHelpers 17 | config.include GithubHelpers 18 | config.include GitReflow::RSpec::StubHelpers 19 | config.include GitReflow::RSpec::WorkflowHelpers 20 | 21 | config.expect_with :rspec do |c| 22 | c.syntax = [:should, :expect] 23 | end 24 | 25 | config.mock_with :rspec do |c| 26 | c.syntax = [:should, :expect] 27 | end 28 | 29 | config.before(:each) do 30 | WebMock.reset! 31 | stub_command_line 32 | suppress_loading_of_external_workflows 33 | GitReflow::Workflow.reset! 34 | allow_message_expectations_on_nil 35 | end 36 | 37 | config.after(:each) do 38 | WebMock.reset! 39 | reset_stubbed_command_line 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/support/fake_github.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/mocks' 2 | require 'webmock' 3 | require 'chronic' 4 | 5 | class FakeGitHub 6 | include WebMock::API 7 | 8 | attr_accessor :repo_owner, :repo_name 9 | 10 | DEFAULT_COMMIT_AUTHOR = "reenhanced".freeze 11 | DEFAULT_COMMIT_TIME = "October 21, 2015 07:28:00".freeze 12 | 13 | # EXAMPLE: 14 | # 15 | # FakeGitHub.new(repo_owner: user, repo_name: repo, 16 | # pull_request: { 17 | # number: existing_pull_request.number, 18 | # comments: [{author: comment_author}] 19 | # }, 20 | # issue: { 21 | # comments: [{author: comment_author}] 22 | # }) 23 | # 24 | def initialize(repo_owner: nil, repo_name: nil, pull_request: {}, issue: {}, commits: [], reviews: []) 25 | raise "FakeGitHub#new: repo_owner AND repo_name keywords are required" unless repo_owner and repo_name 26 | 27 | self.repo_owner = repo_owner 28 | self.repo_name = repo_name 29 | 30 | stub_github_request(:pull_request, pull_request) if pull_request 31 | stub_github_request(:issue, issue) if issue 32 | stub_github_request(:commits, commits) if commits.any? 33 | stub_github_request(:reviews, reviews) if reviews.any? 34 | 35 | if pull_request and (issue.none? or !issue[:comments]) 36 | stub_github_request(:issue, pull_request.merge({comments: []})) 37 | end 38 | 39 | if pull_request and commits.none? 40 | stub_github_request(:commits, [{ 41 | author: pull_request[:owner] || DEFAULT_COMMIT_AUTHOR, 42 | created_at: Chronic.parse(DEFAULT_COMMIT_TIME) 43 | }]) 44 | end 45 | 46 | self 47 | end 48 | 49 | def stub_github_request(object_to_stub, object_data) 50 | case object_to_stub 51 | when :commits 52 | commits_response = Fixture.new('repositories/commits.json.erb', 53 | repo_owner: repo_owner, 54 | repo_name: repo_name, 55 | commits: object_data) 56 | commits_response.to_json_hashie.each_with_index do |commit, index| 57 | stub_request(:get, %r{/repos/#{self.repo_owner}/(#{self.repo_name}/)?commits/#{commit.sha}\?}). 58 | to_return( 59 | body: commit.to_json.to_s, 60 | status: 201, 61 | headers: {content_type: "application/json; charset=utf-8"}) 62 | stub_request(:get, %r{/repos/#{self.repo_owner}/commits\Z}). 63 | to_return( 64 | body: commits_response.to_s, 65 | status: 201, 66 | headers: {content_type: "application/json; charset=utf-8"}) 67 | end 68 | when :issue 69 | # Stubbing issue comments 70 | if object_data[:comments] 71 | stub_request(:get, %r{/repos/#{self.repo_owner}/(#{self.repo_name}/)?issues/#{object_data[:number] || 1}/comments}). 72 | with(query: {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 73 | to_return(body: Fixture.new('issues/comments.json.erb', 74 | repo_owner: self.repo_owner, 75 | repo_name: self.repo_name, 76 | comments: object_data[:comments], 77 | pull_request_number: object_data[:number] || 1, 78 | body: object_data[:body] || 'Hammer time', 79 | created_at: object_data[:created_at] || Chronic.parse(DEFAULT_COMMIT_TIME)).to_s, 80 | status: 201, 81 | headers: {content_type: "application/json; charset=utf-8"}) 82 | else 83 | stub_request(:get, %r{/repos/#{self.repo_owner}/(#{self.repo_name}/)?issues/#{object_data[:number] || 1}/comments}). 84 | with(query: {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 85 | to_return(body: '[]', status: 201, headers: {content_type: "application/json; charset=utf-8"}) 86 | end 87 | when :pull_request 88 | # EXAMPLES 89 | stubbed_pull_request_response = Fixture.new('pull_requests/pull_request.json.erb', 90 | number: object_data[:number] || 1, 91 | title: object_data[:title] || 'Please merge these changes', 92 | body: object_data[:body] || 'Bone saw is ready.', 93 | state: object_data[:state] || 'open', 94 | owner: object_data[:owner] || 'octocat', 95 | feature_repo_owner: object_data[:feature_repo_owner] || self.repo_owner, 96 | feature_branch: object_data[:feature_branch] || 'new-feature', 97 | base_branch: object_data[:base_branch] || 'master', 98 | repo_owner: self.repo_owner, 99 | repo_name: self.repo_name) 100 | 101 | stub_request(:get, "#{GitReflow::GitServer::GitHub.api_endpoint}/repos/#{self.repo_owner}/#{self.repo_name}/pulls/#{object_data[:number]}"). 102 | with(query: {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 103 | to_return(body: stubbed_pull_request_response.to_s, status: 201, headers: {content_type: "application/json; charset=utf-8"}) 104 | stub_request(:get, "#{GitReflow::GitServer::GitHub.api_endpoint}/repos/#{self.repo_owner}/#{self.repo_name}/pulls") 105 | .with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0', 'base' => object_data[:base_branch] || 'master', 'head' => "#{object_data[:feature_repo_owner] || self.repo_owner}:#{object_data[:feature_branch] || "new-feature"}", 'state' => object_data[:state] || 'open'}). 106 | to_return(:body => "[#{stubbed_pull_request_response.to_s}]", :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 107 | 108 | # Stubbing pull request comments 109 | if object_data[:comments] 110 | stub_request(:get, %r{/repos/#{self.repo_owner}/(#{self.repo_name}/)?pulls/#{object_data[:number] || 1}/comments}). 111 | with(query: {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 112 | to_return(body: Fixture.new('pull_requests/comments.json.erb', 113 | repo_owner: self.repo_owner, 114 | repo_name: self.repo_name, 115 | comments: object_data[:comments], 116 | pull_request_number: object_data[:number] || 1, 117 | created_at: object_data[:created_at] || Chronic.parse(DEFAULT_COMMIT_TIME)).to_s, 118 | status: 201, 119 | headers: {content_type: "application/json; charset=utf-8"}) 120 | end 121 | 122 | # Stubbing pull request reviews 123 | if object_data[:reviews] 124 | stub_request(:get, %r{/repos/#{self.repo_owner}/#{self.repo_name}/pulls/#{object_data[:number] || 1}/reviews}). 125 | with(query: {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 126 | to_return(body: Fixture.new('pull_requests/reviews.json.erb', 127 | repo_owner: self.repo_owner, 128 | repo_name: self.repo_name, 129 | reviews: object_data[:reviews], 130 | pull_request_number: object_data[:number] || 1, 131 | body: object_data[:body] || 'Hammer time').to_s, 132 | status: 200, 133 | headers: {content_type: "application/json; charset=utf-8"}) 134 | end 135 | 136 | # Stubbing pull request commits 137 | #stub_get(%r{#{GitReflow::GitServer::GitHub.api_endpoint}/repos/#{user}/#{repo}/pulls/#{existing_pull_request.number}/commits}). 138 | # with(query: {"access_token" => "a1b2c3d4e5f6g7h8i9j0"}). 139 | # to_return(:body => Fixture.new("pull_requests/commits.json").to_s, status: 201, headers: {content_type: "application/json; charset=utf-8"}) 140 | end 141 | end 142 | end 143 | 144 | -------------------------------------------------------------------------------- /spec/support/fixtures.rb: -------------------------------------------------------------------------------- 1 | # ERB parsing credit: 2 | # http://stackoverflow.com/questions/8954706/render-an-erb-template-with-values-from-a-hash/9734736#9734736 3 | 4 | require 'erb' 5 | require 'ostruct' 6 | 7 | class Fixture 8 | attr_reader :file, :locals 9 | 10 | def initialize(file, locals = {}) 11 | @file = fixture(file) 12 | @locals = locals 13 | end 14 | 15 | def fixture_path 16 | File.expand_path("../../fixtures", __FILE__) 17 | end 18 | 19 | def fixture(file) 20 | File.new(File.join(fixture_path, "/", file)) 21 | end 22 | 23 | def to_s 24 | if File.extname(file) == ".erb" 25 | ERB.new(template_file_content).result(OpenStruct.new(locals).instance_eval { binding }).to_s 26 | else 27 | template_file_content.to_s 28 | end 29 | end 30 | 31 | def to_json 32 | if File.extname(file) == ".erb" 33 | rendered_file = ERB.new(template_file_content).result(OpenStruct.new(locals).instance_eval { binding }) 34 | JSON.parse(rendered_file) 35 | else 36 | JSON.parse(template_file_content) 37 | end 38 | end 39 | 40 | def to_json_hashie 41 | json = self.to_json 42 | if json.is_a? Array 43 | json.map {|json_object| Hashie::Mash.new json_object } 44 | else 45 | Hashie::Mash.new json 46 | end 47 | end 48 | 49 | private 50 | 51 | def template_file_content 52 | @file_content ||= file.read 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/support/github_helpers.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << 'lib' 2 | require 'git_reflow' 3 | require 'github_api' 4 | require 'spec_helper' 5 | 6 | module GithubHelpers 7 | def stub_github_with(options = {}) 8 | 9 | hostname = options[:hostname] || 'hostname.local' 10 | api_endpoint = options[:api_endpoint] || "https://api.github.com" 11 | site_url = options[:site_url] || "https://github.com" 12 | user = options[:user] || 'reenhanced' 13 | password = options[:passwordl] || 'shazam' 14 | oauth_token_hash = Hashie::Mash.new({ token: 'a1b2c3d4e5f6g7h8i9j0', note: 'git-reflow (hostname.local)'}) 15 | repo = options[:repo] || 'repo' 16 | branch = options[:branch] || 'new-feature' 17 | pull = options[:pull] 18 | 19 | allow_any_instance_of(HighLine).to receive(:ask) do |terminal, question| 20 | values = { 21 | "Please enter your GitHub username: " => user, 22 | "Please enter your GitHub password (we do NOT store this): " => password, 23 | "Please enter your Enterprise site URL (e.g. https://github.company.com):" => enterprise_site, 24 | "Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):" => enterprise_api 25 | } 26 | return_value = values[question] || values[terminal] 27 | question = "" 28 | return_value 29 | end 30 | 31 | github = Github.new do |config| 32 | config.oauth_token = oauth_token_hash.token 33 | config.endpoint = api_endpoint 34 | config.site = site_url 35 | config.adapter = :net_http 36 | config.ssl = {:verify => false} 37 | end 38 | 39 | stub_request(:get, "#{api_endpoint}/authorizations?").to_return(:body => [oauth_token_hash].to_json, status: 200, headers: {}) 40 | allow(Github::Client).to receive(:new).and_return(github) 41 | allow(GitReflow).to receive(:push_current_branch).and_return(true) 42 | allow(GitReflow).to receive(:git_server).and_return(github) 43 | allow(GitReflow).to receive(:current_branch).and_return(branch) 44 | allow(GitReflow).to receive(:remote_repo_name).and_return(repo) 45 | allow(GitReflow).to receive(:remote_user).and_return(user) 46 | allow(GitReflow).to receive(:fetch_destination).and_return(true) 47 | allow(GitReflow).to receive(:update_destination).and_return(true) 48 | 49 | allow_any_instance_of(GitReflow::GitServer::GitHub).to receive(:run).with('hostname', loud: false).and_return(hostname) 50 | github_server = GitReflow::GitServer::GitHub.new 51 | allow(github_server.class).to receive(:user).and_return(user) 52 | allow(github_server.class).to receive(:oauth_token).and_return(oauth_token_hash.token) 53 | allow(github_server.class).to receive(:site_url).and_return(site_url) 54 | allow(github_server.class).to receive(:api_endpoint).and_return(api_endpoint) 55 | allow(github_server.class).to receive(:remote_user).and_return(user) 56 | allow(github_server.class).to receive(:remote_repo).and_return(repo) 57 | allow(github_server.class).to receive(:oauth_token).and_return(oauth_token_hash.token) 58 | allow(github_server.class).to receive(:get_committed_time).and_return(Time.now) 59 | 60 | allow(GitReflow).to receive(:git_server).and_return(github_server) 61 | 62 | # Stubbing statuses for a given commit 63 | #stub_request(:get, %r{#{GitReflow.git_server.class.api_endpoint}/repos/#{user}/commits/\w+}). 64 | # to_return(:body => Fixture.new('repositories/commit.json.erb', repo_owner: user, repo_name: repo, author: user).to_json.to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 65 | stub_request(:get, %r{/repos/#{user}/(#{repo}/)?commits/\w+/statuses}). 66 | to_return(:body => Fixture.new('repositories/statuses.json').to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 67 | 68 | if pull 69 | # Stubbing review 70 | stub_post("/repos/#{user}/#{repo}/pulls"). 71 | to_return(:body => pull.to_s, :status => 201, :headers => {:content_type => "application/json\; charset=utf-8"}) 72 | 73 | # Stubbing pull request finder 74 | stub_get("/repos/#{user}/#{repo}/pulls/#{pull.number}").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 75 | to_return(:body => Fixture.new('pull_requests/pull_request.json').to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 76 | stub_get("/repos/#{user}/pulls").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0', 'base' => 'master', 'head' => "#{user}:#{branch}", 'state' => 'open'}). 77 | to_return(:body => Fixture.new('pull_requests/pull_requests.json').to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 78 | stub_get("/repos/#{user}/#{repo}/pulls").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0', 'base' => 'master', 'head' => "#{user}:#{branch}", 'state' => 'open'}). 79 | to_return(:body => Fixture.new('pull_requests/pull_requests.json').to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 80 | # Stubbing pull request comments 81 | stub_get("/repos/#{user}/#{repo}/pulls/#{pull.number}/comments?").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 82 | to_return(:body => Fixture.new('pull_requests/comments.json.erb', repo_owner: user, repo_name: repo, comments: [{author: user}], pull_request_number: pull.number).to_json.to_s, :status => 201, :headers => {'Accept' => 'application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1', :content_type => "application/json; charset=utf-8"}) 83 | stub_get("/repos/#{user}/pulls/#{pull.number}/comments?").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 84 | to_return(:body => Fixture.new('pull_requests/comments.json.erb', repo_owner: user, repo_name: repo, comments: [{author: user}], pull_request_number: pull.number).to_s, :status => 201, :headers => {'Accept' => 'application/vnd.github.v3+json,application/vnd.github.beta+json;q=0.5,application/json;q=0.1', :content_type => "application/json; charset=utf-8"}) 85 | # Stubbing issue comments 86 | stub_get("/repos/#{user}/issues/#{pull.number}/comments?").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 87 | to_return(:body => Fixture.new('issues/comments.json.erb', repo_owner: user, repo_name: repo, comments: [{author: user}], pull_request_number: pull.number).to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 88 | stub_get("/repos/#{user}/#{repo}/issues/#{pull.number}/comments?").with(:query => {'access_token' => 'a1b2c3d4e5f6g7h8i9j0'}). 89 | to_return(:body => Fixture.new('issues/comments.json.erb', repo_owner: user, repo_name: repo, comments: [{author: user}], pull_request_number: pull.number).to_json.to_s, :status => 201, :headers => {:content_type => "application/json; charset=utf-8"}) 90 | # Stubbing pull request commits 91 | stub_get("/repos/#{user}/#{repo}/pulls/#{pull.number}/commits").with(query: {"access_token" => "a1b2c3d4e5f6g7h8i9j0"}). 92 | to_return(:body => Fixture.new("pull_requests/commits.json").to_s, status: 201, headers: {content_type: "application/json; charset=utf-8"}) 93 | stub_request(:get, %r{/repos/#{user}/commits/\w+}).with(query: {"access_token" => "a1b2c3d4e5f6g7h8i9j0"}). 94 | to_return(:body => Fixture.new("repositories/commit.json").to_s, status: 201, headers: {content_type: "application/json; charset=utf-8"}) 95 | end 96 | 97 | github_server 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/support/mock_pull_request.rb: -------------------------------------------------------------------------------- 1 | class MockPullRequest < GitReflow::GitServer::PullRequest 2 | DESCRIPTION = "Bingo! Unity." 3 | HTML_URL = "https://github.com/reenhanced/gitreflow/pulls/0" 4 | FEATURE_BRANCH_NAME = "feature_branch" 5 | BASE_BRANCH_NAME = "base" 6 | NUMBER = 0 7 | 8 | def initialize(attributes = Struct.new(:description, :html_url, :feature_branch_name, :base_branch_name, :number).new) 9 | self.description = attributes.description || DESCRIPTION 10 | self.html_url = attributes.html_url || HTML_URL 11 | self.feature_branch_name = attributes.feature_branch_name || FEATURE_BRANCH_NAME 12 | self.base_branch_name = attributes.base_branch_name || BASE_BRANCH_NAME 13 | self.build = Build.new 14 | self.number = attributes.number || NUMBER 15 | self.source_object = attributes 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/web_mocks.rb: -------------------------------------------------------------------------------- 1 | def stub_get(path, endpoint = GitReflow.git_server.class.api_endpoint) 2 | stub_request(:get, endpoint + path) 3 | end 4 | 5 | def stub_post(path, endpoint = GitReflow.git_server.class.api_endpoint) 6 | stub_request(:post, endpoint + path) 7 | end 8 | 9 | def stub_patch(path, endpoint = Github.endpoint.to_s) 10 | stub_request(:patch, endpoint + path) 11 | end 12 | 13 | def stub_put(path, endpoint = Github.endpoint.to_s) 14 | stub_request(:put, endpoint + path) 15 | end 16 | 17 | def stub_delete(path, endpoint = Github.endpoint.to_s) 18 | stub_request(:delete, endpoint + path) 19 | end 20 | 21 | def a_get(path, endpoint = Github.endpoint.to_s) 22 | a_request(:get, endpoint + path) 23 | end 24 | 25 | def a_post(path, endpoint = Github.endpoint.to_s) 26 | a_request(:post, endpoint + path) 27 | end 28 | 29 | def a_patch(path, endpoint = Github.endpoint.to_s) 30 | a_request(:patch, endpoint + path) 31 | end 32 | 33 | def a_put(path, endpoint = Github.endpoint) 34 | a_request(:put, endpoint + path) 35 | end 36 | 37 | def a_delete(path, endpoint = Github.endpoint) 38 | a_request(:delete, endpoint + path) 39 | end 40 | --------------------------------------------------------------------------------