thegarage-gitx 1.5.5.pre1 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Guardfile +8 -0
- data/README.md +2 -0
- data/bin/git-buildtag +3 -2
- data/bin/git-cleanup +4 -2
- data/bin/git-integrate +3 -2
- data/bin/git-nuke +3 -2
- data/bin/git-release +3 -2
- data/bin/git-reviewrequest +3 -2
- data/bin/git-share +3 -2
- data/bin/git-start +3 -2
- data/bin/git-track +3 -2
- data/bin/git-update +3 -2
- data/lib/thegarage/gitx/cli/base_command.rb +74 -0
- data/lib/thegarage/gitx/cli/buildtag_command.rb +39 -0
- data/lib/thegarage/gitx/cli/cleanup_command.rb +36 -0
- data/lib/thegarage/gitx/cli/integrate_command.rb +40 -0
- data/lib/thegarage/gitx/cli/nuke_command.rb +64 -0
- data/lib/thegarage/gitx/cli/release_command.rb +31 -0
- data/lib/thegarage/gitx/cli/review_request_command.rb +203 -0
- data/lib/thegarage/gitx/cli/share_command.rb +17 -0
- data/lib/thegarage/gitx/cli/start_command.rb +31 -0
- data/lib/thegarage/gitx/cli/track_command.rb +16 -0
- data/lib/thegarage/gitx/cli/update_command.rb +23 -0
- data/lib/thegarage/gitx/version.rb +1 -1
- data/lib/thegarage/gitx.rb +0 -2
- data/spec/spec_helper.rb +3 -1
- data/spec/thegarage/gitx/cli/buildtag_command_spec.rb +71 -0
- data/spec/thegarage/gitx/cli/cleanup_command_spec.rb +38 -0
- data/spec/thegarage/gitx/cli/integrate_command_spec.rb +65 -0
- data/spec/thegarage/gitx/cli/nuke_command_spec.rb +105 -0
- data/spec/thegarage/gitx/cli/release_command_spec.rb +56 -0
- data/spec/thegarage/gitx/cli/review_request_command_spec.rb +188 -0
- data/spec/thegarage/gitx/cli/share_command_spec.rb +32 -0
- data/spec/thegarage/gitx/cli/start_command_spec.rb +61 -0
- data/spec/thegarage/gitx/cli/track_command_spec.rb +31 -0
- data/spec/thegarage/gitx/cli/update_command_spec.rb +33 -0
- data/thegarage-gitx.gemspec +3 -0
- metadata +76 -11
- data/lib/thegarage/gitx/cli.rb +0 -117
- data/lib/thegarage/gitx/git.rb +0 -210
- data/lib/thegarage/gitx/github.rb +0 -185
- data/spec/thegarage/gitx/cli_spec.rb +0 -257
- data/spec/thegarage/gitx/git_spec.rb +0 -231
- data/spec/thegarage/gitx/github_spec.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5afd02a7ef9c6fc40e717fae217988d5b0eb0b0e
|
4
|
+
data.tar.gz: 980ae3bf2e94996ef6d1f14599ae7956a66b7c01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48e0396c3a9d39952560d3a96caea10dedd64e9b0989da918863528a25969736073c7955fa1385d5aa0abe34104b579ab182e352ff2b34fa008c0ca8c0fe771d
|
7
|
+
data.tar.gz: 8fbb99c25f264ea137e5b6501ea4e8a565b7fdd39730b04d274764b41b8e051eb98bb17191783a4ddc4a4a81df56afbc62eace8eab65d8e63787f5e02b9a6c09
|
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# thegarage-gitx
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/thegarage/thegarage-gitx.png?branch=master)](https://travis-ci.org/thegarage/thegarage-gitx)
|
4
|
+
[![Code Coverage](https://coveralls.io/repos/thegarage/thegarage-gitx/badge.png)](https://coveralls.io/r/thegarage/thegarage-gitx)
|
5
|
+
[![Code Quality](https://codeclimate.com/github/thegarage/thegarage-gitx/badges)](https://codeclimate.com/github/thegarage/thegarage-gitx)
|
4
6
|
|
5
7
|
Useful Git eXtensions for Development workflow at The Garage.
|
6
8
|
|
data/bin/git-buildtag
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'buildtag_command.rb')
|
4
|
+
args = ARGV.dup.unshift('buildtag')
|
5
|
+
Thegarage::Gitx::Cli::BuildtagCommand.start(args)
|
data/bin/git-cleanup
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'cleanup_command.rb')
|
4
|
+
args = ARGV.dup.unshift('cleanup')
|
5
|
+
Thegarage::Gitx::Cli::CleanupCommand.start(args)
|
6
|
+
|
data/bin/git-integrate
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'integrate_command.rb')
|
4
|
+
args = ARGV.dup.unshift('integrate')
|
5
|
+
Thegarage::Gitx::Cli::IntegrateCommand.start(args)
|
data/bin/git-nuke
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'nuke_command.rb')
|
4
|
+
args = ARGV.dup.unshift('nuke')
|
5
|
+
Thegarage::Gitx::Cli::NukeCommand.start(args)
|
data/bin/git-release
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'release_command.rb')
|
4
|
+
args = ARGV.dup.unshift('release')
|
5
|
+
Thegarage::Gitx::Cli::ReleaseCommand.start(args)
|
data/bin/git-reviewrequest
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'review_request_command.rb')
|
4
|
+
args = ARGV.dup.unshift('reviewrequest')
|
5
|
+
Thegarage::Gitx::Cli::ReviewReqeustCommand.start(args)
|
data/bin/git-share
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'share_command.rb')
|
4
|
+
args = ARGV.dup.unshift('share')
|
5
|
+
Thegarage::Gitx::Cli::ShareCommand.start(args)
|
data/bin/git-start
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'start_command.rb')
|
4
|
+
args = ARGV.dup.unshift('start')
|
5
|
+
Thegarage::Gitx::Cli::StartCommand.start(args)
|
data/bin/git-track
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'track_command.rb')
|
4
|
+
args = ARGV.dup.unshift('track')
|
5
|
+
Thegarage::Gitx::Cli::TrackCommand.start(args)
|
data/bin/git-update
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', '
|
4
|
-
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx/cli', 'update_command.rb')
|
4
|
+
args = ARGV.dup.unshift('update')
|
5
|
+
Thegarage::Gitx::Cli::UpdateCommand.start(args)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'pathname'
|
3
|
+
require 'rugged'
|
4
|
+
require 'English'
|
5
|
+
require 'thegarage/gitx'
|
6
|
+
|
7
|
+
module Thegarage
|
8
|
+
module Gitx
|
9
|
+
module Cli
|
10
|
+
class BaseCommand < Thor
|
11
|
+
include Thor::Actions
|
12
|
+
|
13
|
+
AGGREGATE_BRANCHES = %w( staging prototype )
|
14
|
+
RESERVED_BRANCHES = %w( HEAD master next_release ) + AGGREGATE_BRANCHES
|
15
|
+
add_runtime_options!
|
16
|
+
|
17
|
+
method_option :trace, :type => :boolean, :aliases => '-v'
|
18
|
+
def initialize(*args)
|
19
|
+
super(*args)
|
20
|
+
RestClient.proxy = ENV['HTTPS_PROXY'] if ENV.has_key?('HTTPS_PROXY')
|
21
|
+
RestClient.log = Logger.new(STDOUT) if options[:trace]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def repo
|
27
|
+
@repo ||= begin
|
28
|
+
path = Dir.pwd
|
29
|
+
root_path = Rugged::Repository.discover(path)
|
30
|
+
Rugged::Repository.new(root_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# lookup the current branch of the repo
|
35
|
+
def current_branch
|
36
|
+
repo.branches.find(&:head?)
|
37
|
+
end
|
38
|
+
|
39
|
+
# execute a shell command and raise an error if non-zero exit code is returned
|
40
|
+
# return the string output from the command
|
41
|
+
def run_cmd(cmd, options = {})
|
42
|
+
say "$ #{cmd}"
|
43
|
+
output = `#{cmd}`
|
44
|
+
success = $CHILD_STATUS.to_i == 0
|
45
|
+
fail "#{cmd} failed" unless success || options[:allow_failure]
|
46
|
+
output
|
47
|
+
end
|
48
|
+
|
49
|
+
def aggregate_branch?(branch)
|
50
|
+
AGGREGATE_BRANCHES.include?(branch)
|
51
|
+
end
|
52
|
+
|
53
|
+
def assert_not_protected_branch!(branch, action)
|
54
|
+
raise "Cannot #{action} reserved branch" if RESERVED_BRANCHES.include?(branch) || aggregate_branch?(branch)
|
55
|
+
end
|
56
|
+
|
57
|
+
# retrieve a list of branches
|
58
|
+
def branches(options = {})
|
59
|
+
branches = []
|
60
|
+
args = []
|
61
|
+
args << '-r' if options[:remote]
|
62
|
+
args << "--merged #{options[:merged].is_a?(String) ? options[:merged] : ''}" if options[:merged]
|
63
|
+
output = `git branch #{args.join(' ')}`.split("\n")
|
64
|
+
output.each do |branch|
|
65
|
+
branch = branch.gsub(/\*/, '').strip.split(' ').first
|
66
|
+
branch = branch.split('/').last if options[:remote]
|
67
|
+
branches << branch unless RESERVED_BRANCHES.include?(branch)
|
68
|
+
end
|
69
|
+
branches.uniq
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
|
5
|
+
module Thegarage
|
6
|
+
module Gitx
|
7
|
+
module Cli
|
8
|
+
class BuildtagCommand < BaseCommand
|
9
|
+
TAGGABLE_BRANCHES = %w( master staging )
|
10
|
+
|
11
|
+
desc 'buildtag', 'create a tag for the current Travis-CI build and push it back to origin'
|
12
|
+
def buildtag
|
13
|
+
branch = ENV['TRAVIS_BRANCH']
|
14
|
+
pull_request = ENV['TRAVIS_PULL_REQUEST']
|
15
|
+
|
16
|
+
raise "Unknown branch. ENV['TRAVIS_BRANCH'] is required." unless branch
|
17
|
+
|
18
|
+
if pull_request != 'false'
|
19
|
+
say "Skipping creation of tag for pull request: #{pull_request}"
|
20
|
+
elsif !TAGGABLE_BRANCHES.include?(branch)
|
21
|
+
say "Cannot create build tag for branch: #{branch}. Only #{TAGGABLE_BRANCHES} are supported."
|
22
|
+
else
|
23
|
+
label = "Generated tag from TravisCI build #{ENV['TRAVIS_BUILD_NUMBER']}"
|
24
|
+
create_build_tag(branch, label)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def create_build_tag(branch, label)
|
31
|
+
timestamp = Time.now.utc.strftime '%Y-%m-%d-%H-%M-%S'
|
32
|
+
git_tag = "build-#{branch}-#{timestamp}"
|
33
|
+
run_cmd "git tag #{git_tag} -a -m '#{label}'"
|
34
|
+
run_cmd "git push origin #{git_tag}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
|
5
|
+
module Thegarage
|
6
|
+
module Gitx
|
7
|
+
module Cli
|
8
|
+
class CleanupCommand < BaseCommand
|
9
|
+
desc 'cleanup', 'Cleanup branches that have been merged into master from the repo'
|
10
|
+
def cleanup
|
11
|
+
run_cmd "git checkout #{Thegarage::Gitx::BASE_BRANCH}"
|
12
|
+
run_cmd "git pull"
|
13
|
+
run_cmd 'git remote prune origin'
|
14
|
+
|
15
|
+
say "Deleting local and remote branches that have been merged into "
|
16
|
+
say Thegarage::Gitx::BASE_BRANCH, :green
|
17
|
+
merged_remote_branches.each do |branch|
|
18
|
+
run_cmd "git push origin --delete #{branch}"
|
19
|
+
end
|
20
|
+
merged_local_branches.each do |branch|
|
21
|
+
run_cmd "git branch -d #{branch}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def merged_local_branches
|
28
|
+
branches(:merged => true).reject { |b| aggregate_branch?(b) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def merged_remote_branches
|
32
|
+
branches(:merged => true, :remote => true).reject { |b| aggregate_branch?(b) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
require 'thegarage/gitx/cli/update_command'
|
5
|
+
|
6
|
+
module Thegarage
|
7
|
+
module Gitx
|
8
|
+
module Cli
|
9
|
+
class IntegrateCommand < BaseCommand
|
10
|
+
desc 'integrate', 'integrate the current branch into one of the aggregate development branches (default = staging)'
|
11
|
+
def integrate(target_branch = 'staging')
|
12
|
+
branch = current_branch.name
|
13
|
+
assert_not_protected_branch!(branch, 'integrate') unless aggregate_branch?(target_branch)
|
14
|
+
raise "Only aggregate branches are allowed for integration: #{AGGREGATE_BRANCHES}" unless aggregate_branch?(target_branch) || target_branch == Thegarage::Gitx::BASE_BRANCH
|
15
|
+
|
16
|
+
UpdateCommand.new.update
|
17
|
+
|
18
|
+
say "Integrating "
|
19
|
+
say "#{branch} ", :green
|
20
|
+
say "into "
|
21
|
+
say target_branch, :green
|
22
|
+
|
23
|
+
refresh_branch_from_remote target_branch
|
24
|
+
run_cmd "git pull . #{branch}"
|
25
|
+
run_cmd "git push origin HEAD"
|
26
|
+
run_cmd "git checkout #{branch}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# nuke local branch and pull fresh version from remote repo
|
32
|
+
def refresh_branch_from_remote(target_branch)
|
33
|
+
run_cmd "git branch -D #{target_branch}", :allow_failure => true
|
34
|
+
run_cmd "git fetch origin"
|
35
|
+
run_cmd "git checkout #{target_branch}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
|
5
|
+
module Thegarage
|
6
|
+
module Gitx
|
7
|
+
module Cli
|
8
|
+
class NukeCommand < BaseCommand
|
9
|
+
desc 'nuke', 'nuke the specified aggregate branch and reset it to a known good state'
|
10
|
+
method_option :destination, :type => :string, :aliases => '-d', :desc => 'destination branch to reset to'
|
11
|
+
def nuke(bad_branch)
|
12
|
+
good_branch = options[:destination] || ask("What branch do you want to reset #{bad_branch} to? (default: #{bad_branch})")
|
13
|
+
good_branch = bad_branch if good_branch.length == 0
|
14
|
+
|
15
|
+
last_known_good_tag = current_build_tag(good_branch)
|
16
|
+
return unless yes?("Reset #{bad_branch} to #{last_known_good_tag}? (y/n)", :green)
|
17
|
+
fail "Only aggregate branches are allowed to be reset: #{AGGREGATE_BRANCHES}" unless aggregate_branch?(bad_branch)
|
18
|
+
return if migrations_need_to_be_reverted?(bad_branch, last_known_good_tag)
|
19
|
+
|
20
|
+
say "Resetting "
|
21
|
+
say "#{bad_branch} ", :green
|
22
|
+
say "branch to "
|
23
|
+
say last_known_good_tag, :green
|
24
|
+
|
25
|
+
run_cmd "git checkout #{Thegarage::Gitx::BASE_BRANCH}"
|
26
|
+
run_cmd "git branch -D #{bad_branch}", :allow_failure => true
|
27
|
+
run_cmd "git push origin --delete #{bad_branch}", :allow_failure => true
|
28
|
+
run_cmd "git checkout -b #{bad_branch} #{last_known_good_tag}"
|
29
|
+
run_cmd "git push origin #{bad_branch}"
|
30
|
+
run_cmd "git branch --set-upstream-to origin/#{bad_branch}"
|
31
|
+
run_cmd "git checkout #{Thegarage::Gitx::BASE_BRANCH}"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def migrations_need_to_be_reverted?(bad_branch, last_known_good_tag)
|
37
|
+
return false unless File.exists?('db/migrate')
|
38
|
+
outdated_migrations = run_cmd("git diff #{last_known_good_tag}...#{bad_branch} --name-only db/migrate").split
|
39
|
+
return false if outdated_migrations.empty?
|
40
|
+
|
41
|
+
say "#{bad_branch} contains migrations that may need to be reverted. Ensure any reversable migrations are reverted on affected databases before nuking.", :red
|
42
|
+
say 'Example commands to revert outdated migrations:'
|
43
|
+
outdated_migrations.reverse.each do |migration|
|
44
|
+
version = File.basename(migration).split('_').first
|
45
|
+
say "rake db:migrate:down VERSION=#{version}"
|
46
|
+
end
|
47
|
+
!yes?("Are you sure you want to nuke #{bad_branch}? (y/n) ", :green)
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_build_tag(branch)
|
51
|
+
last_build_tag = build_tags_for_branch(branch).last
|
52
|
+
raise "No known good tag found for branch: #{branch}. Verify tag exists via `git tag -l 'build-#{branch}-*'`" unless last_build_tag
|
53
|
+
last_build_tag
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_tags_for_branch(branch)
|
57
|
+
run_cmd "git fetch --tags"
|
58
|
+
build_tags = run_cmd("git tag -l 'build-#{branch}-*'").split
|
59
|
+
build_tags.sort
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
require 'thegarage/gitx/cli/update_command'
|
5
|
+
require 'thegarage/gitx/cli/integrate_command'
|
6
|
+
require 'thegarage/gitx/cli/cleanup_command'
|
7
|
+
|
8
|
+
module Thegarage
|
9
|
+
module Gitx
|
10
|
+
module Cli
|
11
|
+
class ReleaseCommand < BaseCommand
|
12
|
+
desc 'release', 'release the current branch to production'
|
13
|
+
def release
|
14
|
+
return unless yes?("Release #{current_branch.name} to production? (y/n)", :green)
|
15
|
+
|
16
|
+
branch = current_branch.name
|
17
|
+
assert_not_protected_branch!(branch, 'release')
|
18
|
+
UpdateCommand.new.update
|
19
|
+
|
20
|
+
run_cmd "git checkout #{Thegarage::Gitx::BASE_BRANCH}"
|
21
|
+
run_cmd "git pull origin #{Thegarage::Gitx::BASE_BRANCH}"
|
22
|
+
run_cmd "git pull . #{branch}"
|
23
|
+
run_cmd "git push origin HEAD"
|
24
|
+
|
25
|
+
IntegrateCommand.new.integrate('staging')
|
26
|
+
CleanupCommand.new.cleanup
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Thegarage
|
7
|
+
module Gitx
|
8
|
+
module Cli
|
9
|
+
class ReviewRequestCommand < BaseCommand
|
10
|
+
CLIENT_URL = 'https://github.com/thegarage/thegarage-gitx'
|
11
|
+
PULL_REQUEST_FOOTER = <<-EOS.dedent
|
12
|
+
# Pull Request Protips(tm):
|
13
|
+
# * Include description of how this change accomplishes the task at hand.
|
14
|
+
# * Use GitHub flavored Markdown http://github.github.com/github-flavored-markdown/
|
15
|
+
# * Review CONTRIBUTING.md for recommendations of artifacts, links, images, screencasts, etc.
|
16
|
+
#
|
17
|
+
# This footer will automatically be stripped from the pull request description
|
18
|
+
EOS
|
19
|
+
|
20
|
+
desc "reviewrequest", "Create or update a pull request on github"
|
21
|
+
method_option :description, :type => :string, :aliases => '-d', :desc => 'pull request description'
|
22
|
+
method_option :assignee, :type => :string, :aliases => '-a', :desc => 'pull request assignee'
|
23
|
+
method_option :open, :type => :boolean, :aliases => '-o', :desc => 'open the pull request in a web browser'
|
24
|
+
# @see http://developer.github.com/v3/pulls/
|
25
|
+
def reviewrequest
|
26
|
+
fail 'Github authorization token not found' unless authorization_token
|
27
|
+
|
28
|
+
branch = current_branch.name
|
29
|
+
pull_request = find_pull_request(branch)
|
30
|
+
if pull_request.nil?
|
31
|
+
update
|
32
|
+
changelog = run_cmd "git log #{Thegarage::Gitx::BASE_BRANCH}...#{branch} --no-merges --pretty=format:'* %s%n%b'"
|
33
|
+
pull_request = create_pull_request(branch, changelog, options)
|
34
|
+
say 'Pull request created: '
|
35
|
+
say pull_request['html_url'], :green
|
36
|
+
end
|
37
|
+
assign_pull_request(pull_request, options[:assignee]) if options[:assignee]
|
38
|
+
|
39
|
+
run_cmd "open #{pull_request['html_url']}" if options[:open]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# returns [Hash] data structure of created pull request
|
45
|
+
# request github authorization token
|
46
|
+
# User-Agent is required
|
47
|
+
# store the token in local git config
|
48
|
+
# @returns [String] auth token stored in git (current repo, user config or installed global settings)
|
49
|
+
# @see http://developer.github.com/v3/oauth/#scopes
|
50
|
+
# @see http://developer.github.com/v3/#user-agent-required
|
51
|
+
def authorization_token
|
52
|
+
auth_token = repo.config['thegarage.gitx.githubauthtoken']
|
53
|
+
return auth_token unless auth_token.to_s.blank?
|
54
|
+
|
55
|
+
fail "Github user not configured. Run: `git config --global github.user 'me@email.com'`" unless username
|
56
|
+
password = ask("Github password for #{username}: ", :echo => false)
|
57
|
+
|
58
|
+
client_name = "The Garage Git eXtensions - #{remote_origin_name}"
|
59
|
+
payload = {
|
60
|
+
:scopes => ['repo'],
|
61
|
+
:note => client_name,
|
62
|
+
:note_url => CLIENT_URL
|
63
|
+
}.to_json
|
64
|
+
response = RestClient::Request.new({
|
65
|
+
:url => "https://api.github.com/authorizations",
|
66
|
+
:method => "POST",
|
67
|
+
:user => username,
|
68
|
+
:password => password,
|
69
|
+
:payload => payload,
|
70
|
+
:headers => {
|
71
|
+
:accept => :json,
|
72
|
+
:content_type => :json,
|
73
|
+
:user_agent => 'thegarage/gitx'
|
74
|
+
}
|
75
|
+
}).execute
|
76
|
+
data = JSON.parse response.body
|
77
|
+
token = data['token']
|
78
|
+
repo.config['thegarage.gitx.githubauthtoken'] = token
|
79
|
+
token
|
80
|
+
rescue RestClient::Exception => e
|
81
|
+
process_error e
|
82
|
+
end
|
83
|
+
|
84
|
+
# @see http://developer.github.com/v3/pulls/
|
85
|
+
def create_pull_request(branch, changelog, options = {})
|
86
|
+
body = pull_request_body(changelog, options[:description])
|
87
|
+
|
88
|
+
say "Creating pull request for "
|
89
|
+
say "#{branch} ", :green
|
90
|
+
say "against "
|
91
|
+
say "#{Thegarage::Gitx::BASE_BRANCH} ", :green
|
92
|
+
say "in "
|
93
|
+
say remote_origin_name, :green
|
94
|
+
|
95
|
+
payload = {
|
96
|
+
:title => branch,
|
97
|
+
:base => Thegarage::Gitx::BASE_BRANCH,
|
98
|
+
:head => branch,
|
99
|
+
:body => body
|
100
|
+
}.to_json
|
101
|
+
response = RestClient::Request.new(:url => pull_request_url, :method => "POST", :payload => payload, :headers => request_headers).execute
|
102
|
+
pull_request = JSON.parse response.body
|
103
|
+
|
104
|
+
pull_request
|
105
|
+
rescue RestClient::Exception => e
|
106
|
+
process_error e
|
107
|
+
end
|
108
|
+
|
109
|
+
def assign_pull_request(pull_request, assignee)
|
110
|
+
say "Assigning pull request to "
|
111
|
+
say assignee, :green
|
112
|
+
|
113
|
+
branch = pull_request['head']['ref']
|
114
|
+
payload = {
|
115
|
+
:title => branch,
|
116
|
+
:assignee => assignee
|
117
|
+
}.to_json
|
118
|
+
RestClient::Request.new(:url => pull_request['issue_url'], :method => "PATCH", :payload => payload, :headers => request_headers).execute
|
119
|
+
rescue RestClient::Exception => e
|
120
|
+
process_error e
|
121
|
+
end
|
122
|
+
|
123
|
+
# @returns [Hash] data structure of pull request info if found
|
124
|
+
# @returns nil if no pull request found
|
125
|
+
def find_pull_request(branch)
|
126
|
+
head_reference = [remote_origin_name.split('/').first, branch].join(':')
|
127
|
+
params = {
|
128
|
+
head: head_reference,
|
129
|
+
state: 'open'
|
130
|
+
}
|
131
|
+
response = RestClient::Request.new(:url => pull_request_url, :method => "GET", :headers => request_headers.merge(params: params)).execute
|
132
|
+
data = JSON.parse(response.body)
|
133
|
+
data.first
|
134
|
+
rescue RestClient::Exception => e
|
135
|
+
process_error e
|
136
|
+
end
|
137
|
+
|
138
|
+
def process_error(e)
|
139
|
+
data = JSON.parse e.http_body
|
140
|
+
say "Github request failed: #{data['message']}", :red
|
141
|
+
throw e
|
142
|
+
end
|
143
|
+
|
144
|
+
def pull_request_url
|
145
|
+
"https://api.github.com/repos/#{remote_origin_name}/pulls"
|
146
|
+
end
|
147
|
+
|
148
|
+
def request_headers
|
149
|
+
{
|
150
|
+
:accept => :json,
|
151
|
+
:content_type => :json,
|
152
|
+
'Authorization' => "token #{authorization_token}"
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
# @returns [String] github username (ex: 'wireframe') of the current github.user
|
157
|
+
# @returns empty [String] when no github.user is set on the system
|
158
|
+
def username
|
159
|
+
repo.config['github.user']
|
160
|
+
end
|
161
|
+
|
162
|
+
# lookup the current repository of the PWD
|
163
|
+
# ex: git@github.com:socialcast/thegarage/gitx.git OR https://github.com/socialcast/thegarage/gitx.git
|
164
|
+
def remote_origin_name
|
165
|
+
remote = repo.config['remote.origin.url']
|
166
|
+
remote.to_s.gsub(/\.git$/,'').split(/[:\/]/).last(2).join('/')
|
167
|
+
end
|
168
|
+
|
169
|
+
def pull_request_body(changelog, description = nil)
|
170
|
+
description_template = []
|
171
|
+
description_template << "#{description}\n" if description
|
172
|
+
description_template << '### Changelog'
|
173
|
+
description_template << changelog
|
174
|
+
description_template << PULL_REQUEST_FOOTER
|
175
|
+
|
176
|
+
body = input_from_editor(description_template.join("\n"))
|
177
|
+
body.gsub(PULL_REQUEST_FOOTER, '').chomp.strip
|
178
|
+
end
|
179
|
+
|
180
|
+
# launch configured editor to retreive message/string
|
181
|
+
def input_from_editor(initial_text = '')
|
182
|
+
Tempfile.open('reviewrequest.md') do |f|
|
183
|
+
f << initial_text
|
184
|
+
f.flush
|
185
|
+
|
186
|
+
editor = repo.config['core.editor'] || ENV['EDITOR'] || 'vi'
|
187
|
+
flags = case editor
|
188
|
+
when 'mate', 'emacs', 'subl'
|
189
|
+
'-w'
|
190
|
+
when 'mvim'
|
191
|
+
'-f'
|
192
|
+
else
|
193
|
+
''
|
194
|
+
end
|
195
|
+
pid = fork { exec([editor, flags, f.path].join(' ')) }
|
196
|
+
Process.waitpid(pid)
|
197
|
+
File.read(f.path)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'thegarage/gitx'
|
3
|
+
require 'thegarage/gitx/cli/base_command'
|
4
|
+
|
5
|
+
module Thegarage
|
6
|
+
module Gitx
|
7
|
+
module Cli
|
8
|
+
class ShareCommand < BaseCommand
|
9
|
+
desc 'share', 'Share the current branch in the remote repository'
|
10
|
+
def share
|
11
|
+
run_cmd "git push origin #{current_branch.name}"
|
12
|
+
run_cmd "git branch --set-upstream-to origin/#{current_branch.name}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|