gitx 2.13.1

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 (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