gitx 2.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/CONTRIBUTING.md +67 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +87 -0
  11. data/Rakefile +5 -0
  12. data/bin/git-buildtag +6 -0
  13. data/bin/git-cleanup +7 -0
  14. data/bin/git-integrate +6 -0
  15. data/bin/git-nuke +6 -0
  16. data/bin/git-release +6 -0
  17. data/bin/git-review +6 -0
  18. data/bin/git-share +6 -0
  19. data/bin/git-start +6 -0
  20. data/bin/git-track +6 -0
  21. data/bin/git-update +6 -0
  22. data/gitx.gemspec +37 -0
  23. data/lib/gitx.rb +8 -0
  24. data/lib/gitx/cli/base_command.rb +56 -0
  25. data/lib/gitx/cli/buildtag_command.rb +39 -0
  26. data/lib/gitx/cli/cleanup_command.rb +45 -0
  27. data/lib/gitx/cli/integrate_command.rb +94 -0
  28. data/lib/gitx/cli/nuke_command.rb +62 -0
  29. data/lib/gitx/cli/release_command.rb +40 -0
  30. data/lib/gitx/cli/review_command.rb +93 -0
  31. data/lib/gitx/cli/share_command.rb +15 -0
  32. data/lib/gitx/cli/start_command.rb +37 -0
  33. data/lib/gitx/cli/track_command.rb +14 -0
  34. data/lib/gitx/cli/update_command.rb +35 -0
  35. data/lib/gitx/configuration.rb +44 -0
  36. data/lib/gitx/extensions/string.rb +12 -0
  37. data/lib/gitx/extensions/thor.rb +39 -0
  38. data/lib/gitx/github.rb +178 -0
  39. data/lib/gitx/version.rb +3 -0
  40. data/spec/fixtures/vcr_cassettes/pull_request_does_exist_with_failure_status.yml +135 -0
  41. data/spec/fixtures/vcr_cassettes/pull_request_does_exist_with_success_status.yml +149 -0
  42. data/spec/fixtures/vcr_cassettes/pull_request_does_not_exist.yml +133 -0
  43. data/spec/gitx/cli/base_command_spec.rb +41 -0
  44. data/spec/gitx/cli/buildtag_command_spec.rb +70 -0
  45. data/spec/gitx/cli/cleanup_command_spec.rb +37 -0
  46. data/spec/gitx/cli/integrate_command_spec.rb +290 -0
  47. data/spec/gitx/cli/nuke_command_spec.rb +165 -0
  48. data/spec/gitx/cli/release_command_spec.rb +172 -0
  49. data/spec/gitx/cli/review_command_spec.rb +356 -0
  50. data/spec/gitx/cli/share_command_spec.rb +32 -0
  51. data/spec/gitx/cli/start_command_spec.rb +96 -0
  52. data/spec/gitx/cli/track_command_spec.rb +31 -0
  53. data/spec/gitx/cli/update_command_spec.rb +79 -0
  54. data/spec/spec_helper.rb +86 -0
  55. data/spec/support/global_config.rb +26 -0
  56. data/spec/support/home_env.rb +11 -0
  57. data/spec/support/matchers/meet_expectations_matcher.rb +7 -0
  58. data/spec/support/pry.rb +1 -0
  59. data/spec/support/stub_execution.rb +14 -0
  60. data/spec/support/timecop.rb +9 -0
  61. data/spec/support/vcr.rb +6 -0
  62. data/spec/support/webmock.rb +1 -0
  63. metadata +348 -0
@@ -0,0 +1,45 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class CleanupCommand < BaseCommand
8
+ desc 'cleanup', 'Cleanup branches that have been merged into master from the repo'
9
+ def cleanup
10
+ checkout_branch Gitx::BASE_BRANCH
11
+ run_cmd 'git pull'
12
+ run_cmd 'git remote prune origin'
13
+
14
+ say 'Deleting local and remote branches that have been merged into '
15
+ say Gitx::BASE_BRANCH, :green
16
+ merged_branches(remote: true).each do |branch|
17
+ run_cmd "git push origin --delete #{branch}"
18
+ end
19
+ merged_branches(remote: false).each do |branch|
20
+ run_cmd "git branch -d #{branch}"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @return list of branches that have been merged
27
+ def merged_branches(options = {})
28
+ args = []
29
+ args << '-r' if options[:remote]
30
+ args << '--merged'
31
+ output = run_cmd("git branch #{args.join(' ')}").split("\n")
32
+ branches = output.map do |branch|
33
+ branch = branch.gsub(/\*/, '').strip.split(' ').first
34
+ branch = branch.split('/').last if options[:remote]
35
+ branch
36
+ end
37
+ branches.uniq!
38
+ branches -= config.reserved_branches
39
+ branches.reject! { |b| config.aggregate_branch?(b) }
40
+
41
+ branches
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,94 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+ require 'gitx/cli/update_command'
5
+ require 'gitx/github'
6
+
7
+ module Gitx
8
+ module Cli
9
+ class IntegrateCommand < BaseCommand
10
+ include Gitx::Github
11
+ desc 'integrate', 'integrate the current branch into one of the aggregate development branches (default = staging)'
12
+ method_option :resume, :type => :string, :aliases => '-r', :desc => 'resume merging of feature-branch'
13
+ method_option :comment, :type => :boolean, :aliases => '-c', :desc => 'add a comment to the pull request for this branch. Creates a new PR if none exists.'
14
+ def integrate(integration_branch = 'staging')
15
+ assert_aggregate_branch!(integration_branch)
16
+
17
+ branch = feature_branch_name
18
+ print_message(branch, integration_branch)
19
+
20
+ begin
21
+ execute_command(UpdateCommand, :update)
22
+ rescue
23
+ fail MergeError, 'Merge Conflict Occurred. Please Merge Conflict Occurred. Please fix merge conflict and rerun the integrate command'
24
+ end
25
+
26
+ integrate_branch(branch, integration_branch) unless options[:resume]
27
+ checkout_branch branch
28
+
29
+ create_integrate_comment(branch) if options[:comment] && !config.reserved_branch?(branch)
30
+ end
31
+
32
+ private
33
+
34
+ def print_message(branch, integration_branch)
35
+ message = options[:resume] ? 'Resuming integration of' : 'Integrating'
36
+ say "#{message} "
37
+ say "#{branch} ", :green
38
+ say 'into '
39
+ say integration_branch, :green
40
+ end
41
+
42
+ def integrate_branch(branch, integration_branch)
43
+ fetch_remote_branch(integration_branch)
44
+ begin
45
+ run_cmd "git merge #{branch}"
46
+ rescue
47
+ fail MergeError, "Merge Conflict Occurred. Please fix merge conflict and rerun command with --resume #{branch} flag"
48
+ end
49
+ run_cmd 'git push origin HEAD'
50
+ end
51
+
52
+ def feature_branch_name
53
+ @feature_branch ||= begin
54
+ feature_branch = options[:resume] || current_branch.name
55
+ until local_branch_exists?(feature_branch)
56
+ feature_branch = ask("#{feature_branch} does not exist. Please select one of the available local branches: #{local_branches}")
57
+ end
58
+ feature_branch
59
+ end
60
+ end
61
+
62
+ # nuke local branch and pull fresh version from remote repo
63
+ def fetch_remote_branch(target_branch)
64
+ create_remote_branch(target_branch) unless remote_branch_exists?(target_branch)
65
+ run_cmd 'git fetch origin'
66
+ run_cmd "git branch -D #{target_branch}", :allow_failure => true
67
+ checkout_branch target_branch
68
+ end
69
+
70
+ def local_branch_exists?(branch)
71
+ local_branches.include?(branch)
72
+ end
73
+
74
+ def local_branches
75
+ @local_branches ||= repo.branches.each_name(:local)
76
+ end
77
+
78
+ def remote_branch_exists?(target_branch)
79
+ repo.branches.each_name(:remote).include?("origin/#{target_branch}")
80
+ end
81
+
82
+ def create_remote_branch(target_branch)
83
+ repo.create_branch(target_branch, Gitx::BASE_BRANCH)
84
+ run_cmd "git push origin #{target_branch}:#{target_branch}"
85
+ end
86
+
87
+ def create_integrate_comment(branch)
88
+ pull_request = find_or_create_pull_request(branch)
89
+ comment = '[gitx] integrated into staging :twisted_rightwards_arrows:'
90
+ github_client.add_comment(github_slug, pull_request.number, comment)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,62 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class NukeCommand < BaseCommand
8
+ desc 'nuke', 'nuke the specified aggregate branch and reset it to a known good state'
9
+ method_option :destination, :type => :string, :aliases => '-d', :desc => 'destination branch to reset to'
10
+ def nuke(bad_branch)
11
+ good_branch = options[:destination] || ask("What branch do you want to reset #{bad_branch} to? (default: #{bad_branch})")
12
+ good_branch = bad_branch if good_branch.length == 0
13
+
14
+ last_known_good_tag = current_build_tag(good_branch)
15
+ return unless yes?("Reset #{bad_branch} to #{last_known_good_tag}? (y/n)", :green)
16
+ assert_aggregate_branch!(bad_branch)
17
+ return if migrations_need_to_be_reverted?(bad_branch, last_known_good_tag)
18
+
19
+ say 'Resetting '
20
+ say "#{bad_branch} ", :green
21
+ say 'branch to '
22
+ say last_known_good_tag, :green
23
+
24
+ checkout_branch Gitx::BASE_BRANCH
25
+ run_cmd "git branch -D #{bad_branch}", :allow_failure => true
26
+ run_cmd "git push origin --delete #{bad_branch}", :allow_failure => true
27
+ run_cmd "git checkout -b #{bad_branch} #{last_known_good_tag}"
28
+ run_cmd "git push origin #{bad_branch}"
29
+ run_cmd "git branch --set-upstream-to origin/#{bad_branch}"
30
+ checkout_branch Gitx::BASE_BRANCH
31
+ end
32
+
33
+ private
34
+
35
+ def migrations_need_to_be_reverted?(bad_branch, last_known_good_tag)
36
+ return false unless File.exist?('db/migrate')
37
+ outdated_migrations = run_cmd("git diff #{last_known_good_tag}...#{bad_branch} --name-only db/migrate").split
38
+ return false if outdated_migrations.empty?
39
+
40
+ say "#{bad_branch} contains migrations that may need to be reverted. Ensure any reversable migrations are reverted on affected databases before nuking.", :red
41
+ say 'Example commands to revert outdated migrations:'
42
+ outdated_migrations.reverse.each do |migration|
43
+ version = File.basename(migration).split('_').first
44
+ say "rake db:migrate:down VERSION=#{version}"
45
+ end
46
+ !yes?("Are you sure you want to nuke #{bad_branch}? (y/n) ", :green)
47
+ end
48
+
49
+ def current_build_tag(branch)
50
+ last_build_tag = build_tags_for_branch(branch).last
51
+ raise "No known good tag found for branch: #{branch}. Verify tag exists via `git tag -l 'build-#{branch}-*'`" unless last_build_tag
52
+ last_build_tag
53
+ end
54
+
55
+ def build_tags_for_branch(branch)
56
+ run_cmd 'git fetch --tags'
57
+ build_tags = run_cmd("git tag -l 'build-#{branch}-*'").split
58
+ build_tags.sort
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+ require 'gitx/cli/update_command'
5
+ require 'gitx/cli/integrate_command'
6
+ require 'gitx/cli/cleanup_command'
7
+ require 'gitx/github'
8
+
9
+ module Gitx
10
+ module Cli
11
+ class ReleaseCommand < BaseCommand
12
+ include Gitx::Github
13
+
14
+ desc 'release', 'release the current branch to production'
15
+ method_option :cleanup, :type => :boolean, :desc => 'cleanup merged branches after release'
16
+ def release(branch = nil)
17
+ return unless yes?("Release #{current_branch.name} to production? (y/n)", :green)
18
+
19
+ branch ||= current_branch.name
20
+ assert_not_protected_branch!(branch, 'release')
21
+ checkout_branch(branch)
22
+ execute_command(UpdateCommand, :update)
23
+
24
+ find_or_create_pull_request(branch)
25
+ status = branch_status(branch)
26
+ if status != 'success'
27
+ return unless yes?("Branch status is currently: #{status}. Proceed with release? (y/n)", :red)
28
+ end
29
+
30
+ checkout_branch Gitx::BASE_BRANCH
31
+ run_cmd "git pull origin #{Gitx::BASE_BRANCH}"
32
+ run_cmd "git merge --no-ff #{branch}"
33
+ run_cmd 'git push origin HEAD'
34
+
35
+ execute_command(IntegrateCommand, :integrate, 'staging')
36
+ execute_command(CleanupCommand, :cleanup) if options[:cleanup]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+ require 'gitx/github'
5
+
6
+ module Gitx
7
+ module Cli
8
+ class ReviewCommand < BaseCommand
9
+ include Gitx::Github
10
+
11
+ BUMP_COMMENT_PREFIX = '[gitx] review bump :tada:'
12
+ BUMP_COMMENT_FOOTER = <<-EOS.dedent
13
+ # Bump comments should include:
14
+ # * summary of what changed
15
+ #
16
+ # This footer will automatically be stripped from the created comment
17
+ EOS
18
+ APPROVAL_COMMENT_PREFIX = '[gitx] review approved :shipit:'
19
+ APPROVAL_COMMENT_FOOTER = <<-EOS.dedent
20
+ # Approval comments can include:
21
+ # * feedback
22
+ # * post-release follow-up items
23
+ #
24
+ # This footer will automatically be stripped from the created comment
25
+ EOS
26
+ REJECTION_COMMENT_PREFIX = '[gitx] review rejected'
27
+ REJECTION_COMMENT_FOOTER = <<-EOS.dedent
28
+ # Rejection comments should include:
29
+ # * feedback for fixes required before approved
30
+ #
31
+ # This footer will automatically be stripped from the created comment
32
+ EOS
33
+
34
+ desc 'review', 'Create or update a pull request on github'
35
+ method_option :description, :type => :string, :aliases => '-d', :desc => 'pull request description'
36
+ method_option :assignee, :type => :string, :aliases => '-a', :desc => 'pull request assignee'
37
+ method_option :open, :type => :boolean, :aliases => '-o', :desc => 'open the pull request in a web browser'
38
+ method_option :bump, :type => :boolean, :aliases => '-b', :desc => 'bump an existing pull request by posting a comment to re-review new changes'
39
+ method_option :approve, :type => :boolean, :desc => 'approve the pull request an post comment on pull request'
40
+ method_option :reject, :type => :boolean, :desc => 'reject the pull request an post comment on pull request'
41
+ # @see http://developer.github.com/v3/pulls/
42
+ def review(branch = nil)
43
+ fail 'Github authorization token not found' unless authorization_token
44
+
45
+ branch ||= current_branch.name
46
+ pull_request = find_or_create_pull_request(branch)
47
+ bump_pull_request(pull_request) if options[:bump]
48
+ approve_pull_request(pull_request) if options[:approve]
49
+ reject_pull_request(pull_request) if options[:reject]
50
+ assign_pull_request(pull_request) if options[:assignee]
51
+
52
+ run_cmd "open #{pull_request.html_url}" if options[:open]
53
+ end
54
+
55
+ private
56
+
57
+ def assign_pull_request(pull_request)
58
+ assignee = options[:assignee]
59
+ say 'Assigning pull request to '
60
+ say assignee, :green
61
+
62
+ title = pull_request.title
63
+ body = pull_request.body
64
+ options = {
65
+ assignee: assignee
66
+ }
67
+ github_client.update_issue(github_slug, pull_request.number, title, body, options)
68
+ end
69
+
70
+ def bump_pull_request(pull_request)
71
+ comment = comment_from_template(pull_request, BUMP_COMMENT_PREFIX, BUMP_COMMENT_FOOTER)
72
+ update_review_status(pull_request, 'pending', 'Peer review in progress')
73
+ end
74
+
75
+ def reject_pull_request(pull_request)
76
+ comment = comment_from_template(pull_request, REJECTION_COMMENT_PREFIX, REJECTION_COMMENT_FOOTER)
77
+ update_review_status(pull_request, 'failure', 'Peer review rejected')
78
+ end
79
+
80
+ def approve_pull_request(pull_request)
81
+ comment = comment_from_template(pull_request, APPROVAL_COMMENT_PREFIX, APPROVAL_COMMENT_FOOTER)
82
+ update_review_status(pull_request, 'success', 'Peer review approved')
83
+ end
84
+
85
+ def comment_from_template(pull_request, prefix, footer)
86
+ text = ask_editor("\n\n#{footer}", repo.config['core.editor'])
87
+ comment = [prefix, text].join("\n\n")
88
+ comment = comment.gsub(footer, '').chomp.strip
89
+ github_client.add_comment(github_slug, pull_request.number, comment)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class ShareCommand < BaseCommand
8
+ desc 'share', 'Share the current branch in the remote repository'
9
+ def share
10
+ run_cmd "git push origin #{current_branch.name}"
11
+ run_cmd "git branch --set-upstream-to origin/#{current_branch.name}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class StartCommand < BaseCommand
8
+ EXAMPLE_BRANCH_NAMES = %w( api-fix-invalid-auth desktop-cleanup-avatar-markup share-form-add-edit-link )
9
+ VALID_BRANCH_NAME_REGEX = /^[A-Za-z0-9\-_]+$/
10
+
11
+ desc 'start', 'start a new git branch with latest changes from master'
12
+ def start(branch_name = nil)
13
+ until valid_new_branch_name?(branch_name)
14
+ branch_name = ask("What would you like to name your branch? (ex: #{EXAMPLE_BRANCH_NAMES.sample})")
15
+ end
16
+
17
+ checkout_branch Gitx::BASE_BRANCH
18
+ run_cmd 'git pull'
19
+ repo.create_branch branch_name, Gitx::BASE_BRANCH
20
+ checkout_branch branch_name
21
+ end
22
+
23
+ private
24
+
25
+ def valid_new_branch_name?(branch)
26
+ return false if repo_branches.include?(branch)
27
+ branch =~ VALID_BRANCH_NAME_REGEX
28
+ end
29
+
30
+ def repo_branches
31
+ @branch_names ||= repo.branches.each_name.map do |branch|
32
+ branch.split('/').last
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class TrackCommand < BaseCommand
8
+ desc 'track', 'set the current branch to track the remote branch with the same name'
9
+ def track
10
+ run_cmd "git branch --set-upstream-to origin/#{current_branch.name}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,35 @@
1
+ require 'thor'
2
+ require 'gitx'
3
+ require 'gitx/cli/base_command'
4
+
5
+ module Gitx
6
+ module Cli
7
+ class UpdateCommand < BaseCommand
8
+ desc 'update', 'Update the current branch with latest changes from the remote feature branch and master'
9
+ def update
10
+ say 'Updating '
11
+ say "#{current_branch.name} ", :green
12
+ say 'with latest changes from '
13
+ say Gitx::BASE_BRANCH, :green
14
+
15
+ update_branch(current_branch.name) if remote_branch_exists?(current_branch.name)
16
+ update_branch(Gitx::BASE_BRANCH)
17
+ run_cmd 'git push origin HEAD'
18
+ end
19
+
20
+ private
21
+
22
+ def update_branch(branch)
23
+ begin
24
+ run_cmd "git pull origin #{branch}"
25
+ rescue
26
+ fail MergeError, 'Merge Conflict Occurred. Please fix merge conflict and rerun the update command'
27
+ end
28
+ end
29
+
30
+ def remote_branch_exists?(branch)
31
+ repo.branches.each_name(:remote).include?("origin/#{branch}")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ require 'yaml'
2
+
3
+ module Gitx
4
+ class Configuration
5
+ DEFAULT_CONFIG = {
6
+ 'aggregate_branches' => %w( staging prototype ),
7
+ 'reserved_branches' => %w( HEAD master next_release staging prototype ),
8
+ 'taggable_branches' => %w( master staging )
9
+ }
10
+ CONFIG_FILE = '.gitx.yml'
11
+
12
+ attr_reader :config
13
+
14
+ def initialize(root_dir)
15
+ @config = Thor::CoreExt::HashWithIndifferentAccess.new(DEFAULT_CONFIG)
16
+ config_file_path = File.join(root_dir, CONFIG_FILE)
17
+ @config.merge!(::YAML.load_file(config_file_path)) if File.exist?(config_file_path)
18
+ end
19
+
20
+ def aggregate_branches
21
+ config[:aggregate_branches]
22
+ end
23
+
24
+ def aggregate_branch?(branch)
25
+ aggregate_branches.include?(branch)
26
+ end
27
+
28
+ def reserved_branches
29
+ config[:reserved_branches]
30
+ end
31
+
32
+ def reserved_branch?(branch)
33
+ reserved_branches.include?(branch)
34
+ end
35
+
36
+ def taggable_branches
37
+ config[:taggable_branches]
38
+ end
39
+
40
+ def taggable_branch?(branch)
41
+ taggable_branches.include?(branch)
42
+ end
43
+ end
44
+ end