Skip to content

Commit

Permalink
Merge branch 'master' into staging
Browse files Browse the repository at this point in the history
* master:
  Support releasing feature branch from any repo
  update docs
  bump version
  Support optional branch name for git review command
  Bump version
  Remove unnecessary aggregate_branch method
  Add Thegarage::Gitx::Configuration class
  Fix bug creating double authorizations
  Fix mutating getter antipattern
  remove project name from token note
  DRY up use of paths
  bump version
  Store github authorization token globally
  add debug output
  bump version
  Retry failed authorization instead of exiting.
  Add custom configuration
  • Loading branch information
wireframe committed Mar 16, 2015
2 parents b9e71d1 + 04f7fc4 commit 6a5e2a3
Show file tree
Hide file tree
Showing 19 changed files with 310 additions and 58 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ pkg

## bundler
Gemfile.lock

## RSpec temp files
spec/tmp
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ update the local feature branch with latest remote changes plus upstream release

integrate the current feature branch into an aggregate branch (ex: prototype, staging)

## git review
## git review <feature_branch_name (optional, default: current_branch)>

create a pull request on github for peer review of the current branch. This command is re-runnable
in order to re-assign pull requests.
Expand All @@ -41,13 +41,14 @@ options:
NOTE: the `--bump` option will also update the pull request commit status to mark the branch as 'pending peer review'.
This setting is cleared when a reviewer approves or rejects the pull request.

## git release
## git release <feature_branch_name (optional, default: current_branch)

release the current feature branch to master. This operation will perform the following:
release the feature branch to master. This operation will perform the following:

* pull in latest code from remote branch
* merge in latest code from master branch
* pull latest code from remote feature branch
* pull latest code from master branch
* prompt user to confirm they actually want to perform the release
* check if pull request commit status is currently successful
* merge current branch into master
* (optional) cleanup merged branches from remote server

Expand Down
1 change: 1 addition & 0 deletions lib/thegarage/gitx.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'thegarage/gitx/version'
require 'thegarage/gitx/configuration'
require 'thegarage/gitx/extensions/string'
require 'thegarage/gitx/extensions/thor'

Expand Down
12 changes: 7 additions & 5 deletions lib/thegarage/gitx/cli/base_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ class BaseCommand < Thor

class MergeError < Thor::Error; end

AGGREGATE_BRANCHES = %w( staging prototype )
RESERVED_BRANCHES = %w( HEAD master next_release ) + AGGREGATE_BRANCHES
add_runtime_options!

method_option :trace, :type => :boolean, :aliases => '-v'
Expand All @@ -38,18 +36,22 @@ def current_branch
repo.branches.find(&:head?)
end

def aggregate_branch?(branch)
AGGREGATE_BRANCHES.include?(branch)
def assert_aggregate_branch!(target_branch)
fail "Invalid aggregate branch: #{target_branch} must be one of supported aggregate branches #{config.aggregate_branches}" unless config.aggregate_branch?(target_branch)
end

def assert_not_protected_branch!(branch, action)
raise "Cannot #{action} reserved branch" if RESERVED_BRANCHES.include?(branch) || aggregate_branch?(branch)
raise "Cannot #{action} reserved branch" if config.reserved_branch?(branch) || config.aggregate_branch?(branch)
end

# helper to invoke other CLI commands
def execute_command(command_class, method, args = [])
command_class.new.send(method, *args)
end

def config
@configuration ||= Thegarage::Gitx::Configuration.new(repo.workdir)
end
end
end
end
Expand Down
3 changes: 1 addition & 2 deletions lib/thegarage/gitx/cli/buildtag_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ module Thegarage
module Gitx
module Cli
class BuildtagCommand < BaseCommand
TAGGABLE_BRANCHES = %w( master staging )

desc 'buildtag', 'create a tag for the current build and push it back to origin (supports Travis CI and Codeship)'
def buildtag
fail "Unknown branch. Environment variables TRAVIS_BRANCH or CI_BRANCH are required" unless branch_name
fail "Branch must be one of the supported taggable branches: #{TAGGABLE_BRANCHES}" unless TAGGABLE_BRANCHES.include?(branch_name)
fail "Branch must be one of the supported taggable branches: #{config.taggable_branches}" unless config.taggable_branch?(branch_name)

label = "buildtag generated by build #{build_number}"
create_build_tag(branch_name, label)
Expand Down
4 changes: 2 additions & 2 deletions lib/thegarage/gitx/cli/cleanup_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def merged_branches(options = {})
branch
end
branches.uniq!
branches -= RESERVED_BRANCHES
branches.reject! { |b| aggregate_branch?(b) }
branches -= config.reserved_branches
branches.reject! { |b| config.aggregate_branch?(b) }

branches
end
Expand Down
6 changes: 1 addition & 5 deletions lib/thegarage/gitx/cli/integrate_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def integrate(integration_branch = 'staging')
integrate_branch(branch, integration_branch) unless options[:resume]
checkout_branch branch

create_integrate_comment(branch) unless RESERVED_BRANCHES.include?(branch)
create_integrate_comment(branch) unless config.reserved_branch?(branch)
end

private
Expand Down Expand Up @@ -59,10 +59,6 @@ def feature_branch_name
end
end

def assert_aggregate_branch!(target_branch)
fail "Invalid aggregate branch: #{target_branch} must be one of supported aggregate branches #{AGGREGATE_BRANCHES}" unless aggregate_branch?(target_branch)
end

# nuke local branch and pull fresh version from remote repo
def fetch_remote_branch(target_branch)
create_remote_branch(target_branch) unless remote_branch_exists?(target_branch)
Expand Down
2 changes: 1 addition & 1 deletion lib/thegarage/gitx/cli/nuke_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def nuke(bad_branch)

last_known_good_tag = current_build_tag(good_branch)
return unless yes?("Reset #{bad_branch} to #{last_known_good_tag}? (y/n)", :green)
fail "Only aggregate branches are allowed to be reset: #{AGGREGATE_BRANCHES}" unless aggregate_branch?(bad_branch)
assert_aggregate_branch!(bad_branch)
return if migrations_need_to_be_reverted?(bad_branch, last_known_good_tag)

say "Resetting "
Expand Down
5 changes: 3 additions & 2 deletions lib/thegarage/gitx/cli/release_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ class ReleaseCommand < BaseCommand

desc 'release', 'release the current branch to production'
method_option :cleanup, :type => :boolean, :desc => 'cleanup merged branches after release'
def release
def release(branch = nil)
return unless yes?("Release #{current_branch.name} to production? (y/n)", :green)

branch = current_branch.name
branch ||= current_branch.name
assert_not_protected_branch!(branch, 'release')
checkout_branch(branch)
execute_command(UpdateCommand, :update)

find_or_create_pull_request(branch)
Expand Down
5 changes: 2 additions & 3 deletions lib/thegarage/gitx/cli/review_command.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'thor'
require 'thegarage/gitx'
require 'thegarage/gitx/cli/base_command'
require 'thegarage/gitx/cli/update_command'
require 'thegarage/gitx/github'

module Thegarage
Expand Down Expand Up @@ -41,10 +40,10 @@ class ReviewCommand < BaseCommand
method_option :approve, :type => :boolean, :desc => 'approve the pull request an post comment on pull request'
method_option :reject, :type => :boolean, :desc => 'reject the pull request an post comment on pull request'
# @see http://developer.github.com/v3/pulls/
def review
def review(branch = nil)
fail 'Github authorization token not found' unless authorization_token

branch = current_branch.name
branch ||= current_branch.name
pull_request = find_or_create_pull_request(branch)
bump_pull_request(pull_request) if options[:bump]
approve_pull_request(pull_request) if options[:approve]
Expand Down
47 changes: 47 additions & 0 deletions lib/thegarage/gitx/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'yaml'

module Thegarage
module Gitx
class Configuration
DEFAULT_CONFIG = {
'aggregate_branches' => %w( staging prototype ),
'reserved_branches' => %w( HEAD master next_release staging prototype ),
'taggable_branches' => %w( master staging )
}
CONFIG_FILE = '.gitx.yml'

attr_reader :config

def initialize(root_dir)
@config = Thor::CoreExt::HashWithIndifferentAccess.new(DEFAULT_CONFIG)
config_file_path = File.join(root_dir, CONFIG_FILE)
if File.exists?(config_file_path)
@config.merge!(::YAML::load_file(config_file_path))
end
end

def aggregate_branches
config[:aggregate_branches]
end
def aggregate_branch?(branch)
aggregate_branches.include?(branch)
end

def reserved_branches
config[:reserved_branches]
end

def reserved_branch?(branch)
reserved_branches.include?(branch)
end

def taggable_branches
config[:taggable_branches]
end

def taggable_branch?(branch)
taggable_branches.include?(branch)
end
end
end
end
73 changes: 54 additions & 19 deletions lib/thegarage/gitx/github.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
require 'octokit'
require 'fileutils'
require 'yaml'
require 'thegarage/gitx/cli/update_command'

module Thegarage
module Gitx
module Github
GLOBAL_CONFIG_FILE = '~/.config/gitx/github.yml'
REVIEW_CONTEXT = 'peer_review'
CLIENT_URL = 'https://github.com/thegarage/thegarage-gitx'
PULL_REQUEST_FOOTER = <<-EOS.dedent
Expand All @@ -17,6 +21,7 @@ module Github
def find_or_create_pull_request(branch)
pull_request = find_pull_request(branch)
pull_request ||= begin
checkout_branch(branch)
execute_command(Thegarage::Gitx::Cli::UpdateCommand, :update)
pull_request = create_pull_request(branch)
say 'Created pull request: '
Expand Down Expand Up @@ -79,39 +84,41 @@ def pull_request_body(branch)
body.gsub(PULL_REQUEST_FOOTER, '').chomp.strip
end

# token is cached in local git config for future use
# authorization token used for github API calls
# the token is cached on the filesystem for future use
# @return [String] auth token stored in git (current repo, user config or installed global settings)
# @see http://developer.github.com/v3/oauth/#scopes
# @see http://developer.github.com/v3/#user-agent-required
def authorization_token
auth_token = repo.config['thegarage.gitx.githubauthtoken']
return auth_token unless auth_token.to_s.blank?

auth_token = create_authorization
repo.config['thegarage.gitx.githubauthtoken'] = auth_token
auth_token = global_config['token']
auth_token ||= begin
new_token = create_authorization
save_global_config('token' => new_token)
new_token
end
auth_token
end

def create_authorization
password = ask("Github password for #{username}: ", :echo => false)
say ''
password = ask_without_echo("Github password for #{username}: ")
client = Octokit::Client.new(login: username, password: password)
response = client.create_authorization(authorization_request_options)
response.token
end

def authorization_request_options
timestamp = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z')
client_name = "The Garage Git eXtensions - #{github_slug} #{timestamp}"
options = {
:scopes => ['repo'],
:note => client_name,
:note => github_client_name,
:note_url => CLIENT_URL
}
two_factor_auth_token = ask("Github two factor authorization token (if enabled): ", :echo => false)
say ''
two_factor_auth_token = ask_without_echo("Github two factor authorization token (if enabled): ")
options[:headers] = {'X-GitHub-OTP' => two_factor_auth_token} if two_factor_auth_token
options
response = client.create_authorization(options)
response.token
rescue Octokit::ClientError => e
say "Error creating authorization: #{e.message}", :red
retry
end

def github_client_name
timestamp = Time.now.utc.strftime('%FT%R:%S%z')
client_name = "The Garage Git eXtensions #{timestamp}"
end

def github_client
Expand Down Expand Up @@ -139,6 +146,34 @@ def github_slug
def github_organization
github_slug.split('/').first
end

def global_config_file
File.expand_path(GLOBAL_CONFIG_FILE)
end

def global_config
@config ||= begin
File.exists?(global_config_file) ? YAML.load_file(global_config_file) : {}
end
end

def save_global_config(options)
config_dir = File.dirname(global_config_file)
::FileUtils.mkdir_p(config_dir, mode: 0700) unless File.exists?(config_dir)

@config = global_config.merge(options)
File.open(global_config_file, "a+") do |file|
file.truncate(0)
file.write(@config.to_yaml)
end
File.chmod(0600, global_config_file)
end

def ask_without_echo(message)
value = ask(message, echo: false)
say ''
value
end
end
end
end
2 changes: 1 addition & 1 deletion lib/thegarage/gitx/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Thegarage
module Gitx
VERSION = '2.10.1'
VERSION = '2.13.0'
end
end
24 changes: 24 additions & 0 deletions spec/support/global_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# helper for reading global config file
module GlobalConfig
def global_config_file
config_file = File.join(temp_dir, '.config/gitx/github.yml')
config_dir = File.dirname(config_file)
FileUtils.mkdir_p(config_dir) unless File.exists?(config_dir)
config_file
end
def global_config
YAML.load_file(global_config_file)
end
def temp_dir
tmp_dir = File.join(__dir__, '../tmp')
end
end

RSpec.configure do |config|
config.include GlobalConfig

config.before do
FileUtils.rm_rf(temp_dir)
FileUtils.mkdir_p(temp_dir)
end
end
11 changes: 11 additions & 0 deletions spec/support/home_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Swap home directory to current project spec/tmp
# to allow for writing files + cleanup
RSpec.configure do |config|
config.before do
@old_home = ENV['HOME']
ENV['HOME'] = temp_dir
end
config.after do
ENV['HOME'] = @old_home
end
end

0 comments on commit 6a5e2a3

Please sign in to comment.