thegarage-gitx 1.5.5.pre1 → 2.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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