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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Guardfile +8 -0
  3. data/README.md +2 -0
  4. data/bin/git-buildtag +3 -2
  5. data/bin/git-cleanup +4 -2
  6. data/bin/git-integrate +3 -2
  7. data/bin/git-nuke +3 -2
  8. data/bin/git-release +3 -2
  9. data/bin/git-reviewrequest +3 -2
  10. data/bin/git-share +3 -2
  11. data/bin/git-start +3 -2
  12. data/bin/git-track +3 -2
  13. data/bin/git-update +3 -2
  14. data/lib/thegarage/gitx/cli/base_command.rb +74 -0
  15. data/lib/thegarage/gitx/cli/buildtag_command.rb +39 -0
  16. data/lib/thegarage/gitx/cli/cleanup_command.rb +36 -0
  17. data/lib/thegarage/gitx/cli/integrate_command.rb +40 -0
  18. data/lib/thegarage/gitx/cli/nuke_command.rb +64 -0
  19. data/lib/thegarage/gitx/cli/release_command.rb +31 -0
  20. data/lib/thegarage/gitx/cli/review_request_command.rb +203 -0
  21. data/lib/thegarage/gitx/cli/share_command.rb +17 -0
  22. data/lib/thegarage/gitx/cli/start_command.rb +31 -0
  23. data/lib/thegarage/gitx/cli/track_command.rb +16 -0
  24. data/lib/thegarage/gitx/cli/update_command.rb +23 -0
  25. data/lib/thegarage/gitx/version.rb +1 -1
  26. data/lib/thegarage/gitx.rb +0 -2
  27. data/spec/spec_helper.rb +3 -1
  28. data/spec/thegarage/gitx/cli/buildtag_command_spec.rb +71 -0
  29. data/spec/thegarage/gitx/cli/cleanup_command_spec.rb +38 -0
  30. data/spec/thegarage/gitx/cli/integrate_command_spec.rb +65 -0
  31. data/spec/thegarage/gitx/cli/nuke_command_spec.rb +105 -0
  32. data/spec/thegarage/gitx/cli/release_command_spec.rb +56 -0
  33. data/spec/thegarage/gitx/cli/review_request_command_spec.rb +188 -0
  34. data/spec/thegarage/gitx/cli/share_command_spec.rb +32 -0
  35. data/spec/thegarage/gitx/cli/start_command_spec.rb +61 -0
  36. data/spec/thegarage/gitx/cli/track_command_spec.rb +31 -0
  37. data/spec/thegarage/gitx/cli/update_command_spec.rb +33 -0
  38. data/thegarage-gitx.gemspec +3 -0
  39. metadata +76 -11
  40. data/lib/thegarage/gitx/cli.rb +0 -117
  41. data/lib/thegarage/gitx/git.rb +0 -210
  42. data/lib/thegarage/gitx/github.rb +0 -185
  43. data/spec/thegarage/gitx/cli_spec.rb +0 -257
  44. data/spec/thegarage/gitx/git_spec.rb +0 -231
  45. data/spec/thegarage/gitx/github_spec.rb +0 -91
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1b5057c596d4343322b8f10cb5cc5ffa5d88cc5d
4
- data.tar.gz: 0a0d051f8550bf0e3570e900bfffa4c11bf17e29
3
+ metadata.gz: 5afd02a7ef9c6fc40e717fae217988d5b0eb0b0e
4
+ data.tar.gz: 980ae3bf2e94996ef6d1f14599ae7956a66b7c01
5
5
  SHA512:
6
- metadata.gz: 363c8537bd5f8a1332a523532d4554f5b7ae72a7cab3c5e943eaf7dbd9239ab1bbe7baac6bc7a97aa8f8f8d86d1992c6c1c19e5bee3c9ed007ac0ce1ab6c28fb
7
- data.tar.gz: a6263f625f0c130626419a035d90e8f64c6e7066ff738a1be3dfd3af4c6df791972aff7e81f96ad01859a1195977467d7c8d90827c11cfda5d08c045778d8f9a
6
+ metadata.gz: 48e0396c3a9d39952560d3a96caea10dedd64e9b0989da918863528a25969736073c7955fa1385d5aa0abe34104b579ab182e352ff2b34fa008c0ca8c0fe771d
7
+ data.tar.gz: 8fbb99c25f264ea137e5b6501ea4e8a565b7fdd39730b04d274764b41b8e051eb98bb17191783a4ddc4a4a81df56afbc62eace8eab65d8e63787f5e02b9a6c09
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['buildtag'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['cleanup'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['integrate'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['nuke'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['release'] + ARGV)
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)
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require File.join(File.dirname(__FILE__), '..', 'lib', 'thegarage/gitx', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['reviewrequest'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['share'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['start'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['track'] + ARGV)
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', 'cli.rb')
4
- Thegarage::Gitx::CLI.start (['update'] + ARGV)
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