octopolo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +5 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.markdown +55 -0
  10. data/Rakefile +38 -0
  11. data/bash_completion.sh +13 -0
  12. data/bin/octopolo +21 -0
  13. data/bin/op +21 -0
  14. data/lib/octopolo.rb +15 -0
  15. data/lib/octopolo/changelog.rb +27 -0
  16. data/lib/octopolo/cli.rb +210 -0
  17. data/lib/octopolo/commands/accept_pull.rb +8 -0
  18. data/lib/octopolo/commands/compare_release.rb +9 -0
  19. data/lib/octopolo/commands/deployable.rb +8 -0
  20. data/lib/octopolo/commands/github_auth.rb +5 -0
  21. data/lib/octopolo/commands/new_branch.rb +9 -0
  22. data/lib/octopolo/commands/new_deployable.rb +8 -0
  23. data/lib/octopolo/commands/new_staging.rb +8 -0
  24. data/lib/octopolo/commands/octopolo_setup.rb +5 -0
  25. data/lib/octopolo/commands/pivotal_auth.rb +5 -0
  26. data/lib/octopolo/commands/pull_request.rb +13 -0
  27. data/lib/octopolo/commands/signoff.rb +10 -0
  28. data/lib/octopolo/commands/stage_up.rb +8 -0
  29. data/lib/octopolo/commands/stale_branches.rb +11 -0
  30. data/lib/octopolo/commands/sync_branch.rb +11 -0
  31. data/lib/octopolo/commands/tag_release.rb +13 -0
  32. data/lib/octopolo/config.rb +146 -0
  33. data/lib/octopolo/convenience_wrappers.rb +46 -0
  34. data/lib/octopolo/dated_branch_creator.rb +81 -0
  35. data/lib/octopolo/git.rb +262 -0
  36. data/lib/octopolo/github.rb +95 -0
  37. data/lib/octopolo/github/commit.rb +45 -0
  38. data/lib/octopolo/github/pull_request.rb +126 -0
  39. data/lib/octopolo/github/pull_request_creator.rb +127 -0
  40. data/lib/octopolo/github/user.rb +40 -0
  41. data/lib/octopolo/jira/story_commenter.rb +26 -0
  42. data/lib/octopolo/pivotal.rb +44 -0
  43. data/lib/octopolo/pivotal/story_commenter.rb +19 -0
  44. data/lib/octopolo/pull_request_merger.rb +99 -0
  45. data/lib/octopolo/renderer.rb +37 -0
  46. data/lib/octopolo/reports.rb +18 -0
  47. data/lib/octopolo/scripts.rb +23 -0
  48. data/lib/octopolo/scripts/accept_pull.rb +67 -0
  49. data/lib/octopolo/scripts/compare_release.rb +52 -0
  50. data/lib/octopolo/scripts/deployable.rb +27 -0
  51. data/lib/octopolo/scripts/github_auth.rb +87 -0
  52. data/lib/octopolo/scripts/new_branch.rb +34 -0
  53. data/lib/octopolo/scripts/new_deployable.rb +14 -0
  54. data/lib/octopolo/scripts/new_staging.rb +15 -0
  55. data/lib/octopolo/scripts/octopolo_setup.rb +55 -0
  56. data/lib/octopolo/scripts/pivotal_auth.rb +44 -0
  57. data/lib/octopolo/scripts/pull_request.rb +127 -0
  58. data/lib/octopolo/scripts/signoff.rb +85 -0
  59. data/lib/octopolo/scripts/stage_up.rb +26 -0
  60. data/lib/octopolo/scripts/stale_branches.rb +54 -0
  61. data/lib/octopolo/scripts/sync_branch.rb +37 -0
  62. data/lib/octopolo/scripts/tag_release.rb +70 -0
  63. data/lib/octopolo/templates/pull_request_body.erb +24 -0
  64. data/lib/octopolo/user_config.rb +112 -0
  65. data/lib/octopolo/version.rb +3 -0
  66. data/lib/octopolo/week.rb +130 -0
  67. data/octopolo.gemspec +31 -0
  68. data/spec/.DS_Store +0 -0
  69. data/spec/octopolo/cli_spec.rb +310 -0
  70. data/spec/octopolo/config_spec.rb +344 -0
  71. data/spec/octopolo/convenience_wrappers_spec.rb +80 -0
  72. data/spec/octopolo/dated_branch_creator_spec.rb +143 -0
  73. data/spec/octopolo/git_spec.rb +419 -0
  74. data/spec/octopolo/github/commit_spec.rb +59 -0
  75. data/spec/octopolo/github/pull_request_creator_spec.rb +174 -0
  76. data/spec/octopolo/github/pull_request_spec.rb +291 -0
  77. data/spec/octopolo/github/user_spec.rb +65 -0
  78. data/spec/octopolo/github_spec.rb +169 -0
  79. data/spec/octopolo/jira/stor_commenter_spec.rb +30 -0
  80. data/spec/octopolo/pivotal/story_commenter_spec.rb +34 -0
  81. data/spec/octopolo/pivotal_spec.rb +61 -0
  82. data/spec/octopolo/pull_request_merger_spec.rb +144 -0
  83. data/spec/octopolo/renderer_spec.rb +35 -0
  84. data/spec/octopolo/scripts/accept_pull_spec.rb +76 -0
  85. data/spec/octopolo/scripts/compare_release_spec.rb +115 -0
  86. data/spec/octopolo/scripts/deployable_spec.rb +52 -0
  87. data/spec/octopolo/scripts/github_auth_spec.rb +156 -0
  88. data/spec/octopolo/scripts/new_branch_spec.rb +41 -0
  89. data/spec/octopolo/scripts/new_deployable_spec.rb +18 -0
  90. data/spec/octopolo/scripts/new_staging_spec.rb +18 -0
  91. data/spec/octopolo/scripts/octopolo_setup_spec.rb +120 -0
  92. data/spec/octopolo/scripts/pivotal_auth_spec.rb +77 -0
  93. data/spec/octopolo/scripts/pull_request_spec.rb +217 -0
  94. data/spec/octopolo/scripts/signoff_spec.rb +139 -0
  95. data/spec/octopolo/scripts/stage_up_spec.rb +52 -0
  96. data/spec/octopolo/scripts/stale_branches_spec.rb +81 -0
  97. data/spec/octopolo/scripts/sync_branch_spec.rb +57 -0
  98. data/spec/octopolo/scripts/tag_release_spec.rb +108 -0
  99. data/spec/octopolo/user_config_spec.rb +167 -0
  100. data/spec/octopolo_spec.rb +7 -0
  101. data/spec/spec_helper.rb +29 -0
  102. data/spec/support/engine_yard.cache +0 -0
  103. data/spec/support/sample_octopolo.yml +2 -0
  104. data/spec/support/sample_user.yml +2 -0
  105. data/templates/lib.erb +23 -0
  106. data/templates/script.erb +7 -0
  107. data/templates/spec.erb +29 -0
  108. metadata +344 -0
@@ -0,0 +1,8 @@
1
+ arg :pull_request_id
2
+ desc 'Accept pull requests. Merges the given pull request into master and updates the changelog.'
3
+ command 'accept-pull' do |c|
4
+ c.action do |global_options, options, args|
5
+ require_relative '../scripts/accept_pull'
6
+ Octopolo::Scripts::AcceptPull.execute args.first
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ arg :start, :name => 'starting_tag', :optional => true
2
+ arg :stop, :name => 'ending_tag', :optional => true
3
+ desc 'Opens up a link to compare releases'
4
+ command 'compare-release' do |c|
5
+ c.action do |global_options, options, args|
6
+ require_relative '../scripts/compare_release'
7
+ Octopolo::Scripts::CompareRelease.execute args[0], args[1]
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ arg :pull_request_id
2
+ desc 'Merges PR into the deployable branch'
3
+ command 'deployable' do |c|
4
+ c.action do |global_options, options, args|
5
+ require_relative '../scripts/deployable'
6
+ Octopolo::Scripts::Deployable.execute args.first
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ desc 'Generate a GitHub auth token for octopolo commands to use.'
2
+ command 'github-auth' do |c|
3
+ require_relative '../scripts/github_auth'
4
+ c.action { Octopolo::Scripts::GithubAuth.execute }
5
+ end
@@ -0,0 +1,9 @@
1
+ arg :new_branch_name, :name => 'new_branch_name'
2
+ arg :source_branch_name, :name => 'source_branch_name', :optional => true
3
+ desc 'Create a new branch for features, bug fixes, or experimentation.'
4
+ command 'new-branch' do |c|
5
+ c.action do |global_options, options, args|
6
+ require_relative '../scripts/new_branch'
7
+ Octopolo::Scripts::NewBranch.execute args[0], args[1]
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ desc "Create a new deployable branch"
2
+ long_desc "Create a new deployable branch with today's date and remove the others.
3
+
4
+ Useful when we have changes in the current deployable branch that we wish to remove."
5
+ command 'new-deployable' do |c|
6
+ require_relative '../scripts/new_deployable'
7
+ c.action { Octopolo::Scripts::NewDeployable.new.execute }
8
+ end
@@ -0,0 +1,8 @@
1
+ desc "Create a new staging branch"
2
+ long_desc "Create a new staging branch with today's date and remove the others.
3
+
4
+ Useful when we have changes in the current staging branch that we wish to remove."
5
+ command 'new-staging' do |c|
6
+ require_relative '../scripts/new_staging'
7
+ c.action { Octopolo::Scripts::NewStaging.new.execute }
8
+ end
@@ -0,0 +1,5 @@
1
+ desc 'Basic setup for octoplo'
2
+ command 'setup' do |c|
3
+ require_relative '../scripts/octopolo_setup'
4
+ c.action { Octopolo::Scripts::OctopoloSetup.invoke }
5
+ end
@@ -0,0 +1,5 @@
1
+ desc 'Generate a Pivotal Tracker auth token for Octopolo commands to use.'
2
+ command 'pivotal-auth' do |c|
3
+ require_relative '../scripts/pivotal_auth'
4
+ c.action { Octopolo::Scripts::PivotalAuth.execute }
5
+ end
@@ -0,0 +1,13 @@
1
+ desc "Create a pull request from the current branch to the application's designated deploy branch."
2
+ command 'pull-request' do |c|
3
+ config = Octopolo::Config.parse
4
+
5
+ c.desc "Branch to create the pull request against"
6
+ c.flag [:d, :dest, :destination], :arg_name => "destination_branch", :default_value => config.deploy_branch
7
+
8
+ c.action do |global_options, options, args|
9
+ require_relative '../scripts/pull_request'
10
+ options = global_options.merge(options)
11
+ Octopolo::Scripts::PullRequest.execute options[:destination_branch]
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ arg :pull_request_id
2
+
3
+ desc 'Provide standardized signoff message to a pull request.'
4
+ long_desc "pull_request_id - The ID of the pull request to sign off on"
5
+ command 'signoff' do |c|
6
+ c.action do |global_options, options, args|
7
+ require_relative '../scripts/signoff'
8
+ Octopolo::Scripts::Signoff.execute args.first
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ arg :pull_request_id
2
+ desc 'Merges PR into the staging branch'
3
+ command 'stage-up' do |c|
4
+ c.action do |global_options, options, args|
5
+ require_relative '../scripts/stage_up'
6
+ Octopolo::Scripts::StageUp.execute args.first
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ desc "View and delete stale branches"
2
+ command 'stale-branches' do |c|
3
+ c.desc "Delete the stale branches (default: false)"
4
+ c.switch :delete, :negatable => false
5
+
6
+ c.action do |global_options, options, args|
7
+ require_relative '../scripts/stale_branches'
8
+ options = global_options.merge(options)
9
+ Octopolo::Scripts::StaleBranches.new(options[:delete]).execute
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ config = Octopolo::Config.parse
2
+ long_desc "branch - Which branch to merge into yours (default: #{config.deploy_branch})"
3
+
4
+ arg :branch
5
+ desc "Merge the #{config.deploy_branch} branch into the current working branch"
6
+ command 'sync-branch' do |c|
7
+ c.action do |global_options, options, args|
8
+ require_relative '../scripts/sync_branch'
9
+ Octopolo::Scripts::SyncBranch.execute args.first
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ arg :suffix, :desc => "Suffix to apply to to the dated tag"
2
+
3
+ desc "Create and push a timestamped tag with an optional suffix"
4
+ command 'tag-release' do |c|
5
+ c.desc "Create tag even if not on deploy branch"
6
+ c.switch :force, :negatable => false
7
+
8
+ c.action do |global_options, options, args|
9
+ require_relative '../scripts/tag_release'
10
+ options = global_options.merge(options)
11
+ Octopolo::Scripts::TagRelease.execute args.first, options[:force]
12
+ end
13
+ end
@@ -0,0 +1,146 @@
1
+ require "date" # necessary to get the Date.today convenience method
2
+ require "yaml"
3
+ require_relative "user_config"
4
+
5
+ module Octopolo
6
+ class Config
7
+ FILE_NAMES = %w[.octopolo.yml .automation.yml]
8
+
9
+ RECENT_TAG_LIMIT = 9
10
+ # we use date-based tags, so look for anything starting with a 4-digit year
11
+ RECENT_TAG_FILTER = /^\d{4}.*/
12
+
13
+ attr_accessor :cli
14
+
15
+ def initialize(attributes={})
16
+ self.cli = Octopolo::CLI
17
+
18
+ assign attributes
19
+ load_plugins
20
+ end
21
+
22
+ # default values for these customizations
23
+ def deploy_branch
24
+ @deploy_branch || "master"
25
+ end
26
+
27
+ def branches_to_keep
28
+ @branches_to_keep || []
29
+ end
30
+
31
+ def deploy_environments
32
+ @deploy_environments || []
33
+ end
34
+
35
+ def deploy_methods
36
+ @deploy_methods || []
37
+ end
38
+
39
+ def github_repo
40
+ @github_repo || raise(MissingRequiredAttribute, "GitHub Repo is required")
41
+ end
42
+
43
+ def user_notifications
44
+ if [NilClass, Array, String].include?(@user_notifications.class)
45
+ Array(@user_notifications) if @user_notifications
46
+ else
47
+ raise(InvalidAttributeSupplied, "User notifications must be an array or string")
48
+ end
49
+ end
50
+
51
+ def plugins
52
+ case @plugins
53
+ when Array, String then Array(@plugins)
54
+ when NilClass then []
55
+ else
56
+ raise(InvalidAttributeSupplied, "Plugins must be an array or string")
57
+ end
58
+ end
59
+
60
+ def use_pivotal_tracker
61
+ !!@use_pivotal_tracker
62
+ end
63
+
64
+ def use_jira
65
+ !!@use_jira
66
+ end
67
+
68
+ def jira_user
69
+ @jira_user || raise(MissingRequiredAttribute, "Jira User is required") if use_jira
70
+ end
71
+
72
+ def jira_password
73
+ @jira_password || raise(MissingRequiredAttribute, "Jira Password is required") if use_jira
74
+ end
75
+
76
+ def jira_url
77
+ @jira_url || raise(MissingRequiredAttribute, "Jira Url is required") if use_jira
78
+ end
79
+
80
+ # end defaults
81
+
82
+ def self.parse
83
+ new(attributes_from_file)
84
+ end
85
+
86
+ def self.attributes_from_file
87
+ YAML.load_file(octopolo_config_path)
88
+ end
89
+
90
+ def self.octopolo_config_path
91
+ if filepath = FILE_NAMES.detect {|filename| File.exists?(filename)}
92
+ File.join(Dir.pwd, filepath)
93
+ else
94
+ old_dir = Dir.pwd
95
+ Dir.chdir('..')
96
+ if old_dir != Dir.pwd
97
+ octopolo_config_path
98
+ else
99
+ Octopolo::CLI.say "Could not find #{FILE_NAMES.join(' or ')}"
100
+ exit
101
+ end
102
+ end
103
+ end
104
+
105
+ def load_plugins
106
+ plugins.each do |plugin|
107
+ begin
108
+ require plugin
109
+ rescue LoadError
110
+ puts "Plugin '#{plugin}' failed to load"
111
+ end
112
+ end
113
+ end
114
+
115
+ def assign(attributes)
116
+ attributes.each do |key, value|
117
+ self.instance_variable_set("@#{key}", value)
118
+ end
119
+ end
120
+
121
+ def basedir
122
+ File.basename File.dirname Config.octopolo_config_path
123
+ end
124
+
125
+ def remote_branch_exists?(branch)
126
+ branches = Octopolo::CLI.perform "git branch -r", false
127
+ branch_list = branches.split(/\r?\n/)
128
+ branch_list.each { |x| x.gsub!(/\*|\s/,'') }
129
+ branch_list.include? "origin/#{branch}"
130
+ end
131
+
132
+ def app_name
133
+ basedir
134
+ end
135
+
136
+ # To be used when attempting to call a Config attribute for which there is
137
+ # a value supplied that is of not the correct type
138
+ InvalidAttributeSupplied = Class.new(StandardError)
139
+ # To be used when attempting to call a Config attribute for which there is
140
+ # no sensible default and one hasn't been supplied by the app
141
+ MissingRequiredAttribute = Class.new(StandardError)
142
+ # To be used when looking for a branch of a given type (like staging or
143
+ # deployable), but none exist.
144
+ NoBranchOfType = Class.new(StandardError)
145
+ end
146
+ end
@@ -0,0 +1,46 @@
1
+ module Octopolo
2
+ # Provide access to the CLI class into other classes in the application
3
+ module CLIWrapper
4
+ attr_accessor :cli
5
+
6
+ # Public: Wrapper method around CLI class
7
+ #
8
+ # Returns the CLI class or equivalent
9
+ def cli
10
+ @cli ||= CLI
11
+ end
12
+ end
13
+
14
+ # Provide access to the config into other classes in the application
15
+ module ConfigWrapper
16
+ attr_accessor :config
17
+
18
+ # Public: Wrapper around the user's and app's configuration
19
+ #
20
+ # Returns an instance of Config or equivalent
21
+ def config
22
+ @config ||= Octopolo.config
23
+ end
24
+ end
25
+
26
+ # Provide access to user-supplied configuration values
27
+ module UserConfigWrapper
28
+ attr_accessor :user_config
29
+
30
+ # Returns an instance of UserConfig or equivalent
31
+ def user_config
32
+ @user_config ||= UserConfig.parse
33
+ end
34
+ end
35
+
36
+ module GitWrapper
37
+ attr_accessor :git
38
+
39
+ # Public: Wrapper method around Git class
40
+ #
41
+ # Returns the Git class or equivalent
42
+ def git
43
+ @git ||= Git
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,81 @@
1
+ require_relative "git"
2
+ require_relative "scripts/new_branch"
3
+ require "date"
4
+
5
+ module Octopolo
6
+ class DatedBranchCreator
7
+ include ConfigWrapper
8
+ include CLIWrapper
9
+ include GitWrapper
10
+
11
+ attr_accessor :branch_type
12
+
13
+ # Public: Initialize a new instance of DatedBranchCreator
14
+ #
15
+ # branch_type - Name of the type of branch (e.g., staging or deployable)
16
+ def initialize(branch_type)
17
+ self.branch_type = branch_type
18
+ end
19
+
20
+ # Public: Create a new branch of the given type for today's date
21
+ #
22
+ # branch_type - Name of the type of branch (e.g., staging or deployable)
23
+ #
24
+ # Returns a DatedBranchCreator
25
+ def self.perform(branch_type)
26
+ new(branch_type).tap do |creator|
27
+ creator.perform
28
+ end
29
+ end
30
+
31
+ # Public: Create the branch and handle related processing
32
+ def perform
33
+ create_branch
34
+ delete_old_branches
35
+ end
36
+
37
+ # Public: Create the desired branch
38
+ def create_branch
39
+ git.new_branch(branch_name, config.deploy_branch)
40
+ end
41
+
42
+ # Public: The date suffix to append to the branch name
43
+ def date_suffix
44
+ Date.today.strftime("%Y.%m.%d")
45
+ end
46
+
47
+ # Public: The name of the branch to create
48
+ def branch_name
49
+ case branch_type
50
+ when Git::DEPLOYABLE_PREFIX, Git::STAGING_PREFIX, Git::QAREADY_PREFIX
51
+ "#{branch_type}.#{date_suffix}"
52
+ else
53
+ raise InvalidBranchType, "'#{branch_type}' is not a valid branch type"
54
+ end
55
+ end
56
+
57
+ # Public: If necessary, and if user opts to, delete old branches of its type
58
+ def delete_old_branches
59
+ if extra_branches.any? && cli.ask_boolean("Do you want to delete the old #{branch_type} branch(es)? (#{extra_branches.join(", ")})")
60
+ extra_branches.each do |extra|
61
+ Git.delete_branch(extra)
62
+ end
63
+ end
64
+ end
65
+
66
+ # Public: The list of extra branches that exist after creating the new branch
67
+ #
68
+ # Returns an Array of Strings of the branch names
69
+ def extra_branches
70
+ case branch_type
71
+ when Git::DEPLOYABLE_PREFIX, Git::STAGING_PREFIX, Git::QAREADY_PREFIX
72
+ Git.branches_for(branch_type) - [branch_name]
73
+ else
74
+ raise InvalidBranchType, "'#{branch_type}' is not a valid branch type"
75
+ end
76
+ end
77
+
78
+ InvalidBranchType = Class.new(StandardError)
79
+ end
80
+ end
81
+
@@ -0,0 +1,262 @@
1
+ module Octopolo
2
+ # Abstraction around local Git commands
3
+ class Git
4
+ NO_BRANCH = "(no branch)"
5
+ DEFAULT_DIRTY_MESSAGE = "Your Git index is not clean. Commit, stash, or otherwise clean up the index before continuing."
6
+ # we use date-based tags, so look for anything starting with a 4-digit year
7
+ RELEASE_TAG_FILTER = /^\d{4}.*/
8
+ RECENT_TAG_LIMIT = 9
9
+ # branch prefixes
10
+ DEPLOYABLE_PREFIX = "deployable"
11
+ STAGING_PREFIX = "staging"
12
+ QAREADY_PREFIX = "qaready"
13
+
14
+ include CLIWrapper
15
+ extend CLIWrapper # add class-level .cli and .cli= methods
16
+
17
+ # Public: Perform the given Git subcommand
18
+ #
19
+ # subcommand - String containing the subcommand and its parameters
20
+ #
21
+ # Example:
22
+ #
23
+ # > Git.perform "status"
24
+ # # => output of `git status`
25
+ def self.perform(subcommand)
26
+ cli.perform "git #{subcommand}"
27
+ end
28
+
29
+ # Public: Perform the given Git subcommand without displaying the output
30
+ #
31
+ # subcommand - String containing the subcommand and its parameters
32
+ #
33
+ # Example:
34
+ #
35
+ # > Git.perform_quietly "status"
36
+ # # => no output
37
+ def self.perform_quietly(subcommand)
38
+ cli.perform_quietly "git #{subcommand}"
39
+ end
40
+
41
+ # Public: The name of the currently check-out branch
42
+ #
43
+ # Returns a String of the branch name
44
+ def self.current_branch
45
+ # cut trims the first three characters (whitespace or "* " for current branch)
46
+ # the chomp removes the newline from the command output
47
+ name = cli.perform_quietly("git branch | grep '^* ' | cut -c 3-").chomp
48
+ if name == NO_BRANCH
49
+ raise NotOnBranch, "Not currently checked out to a particular branch"
50
+ else
51
+ name
52
+ end
53
+ end
54
+
55
+ # Public: Determine if current_branch is reserved
56
+ #
57
+ # Returnsa boolean value
58
+ def self.reserved_branch?
59
+ !(current_branch =~ /^(?:#{Git::STAGING_PREFIX}|#{Git::DEPLOYABLE_PREFIX}|#{Git::QAREADY_PREFIX})/).nil?
60
+ end
61
+
62
+ # Public: Check out the given branch name
63
+ #
64
+ # branch_name - The name of the branch to check out
65
+ def self.check_out branch_name
66
+ fetch
67
+ perform "checkout #{branch_name}"
68
+ pull
69
+ unless current_branch == branch_name
70
+ raise CheckoutFailed, "Failed to check out '#{branch_name}'"
71
+ end
72
+ end
73
+
74
+ # Public: Create a new branch from the given source
75
+ #
76
+ # new_branch_name - The name of the branch to create
77
+ # source_branch_name - The name of the branch to branch from
78
+ #
79
+ # Example:
80
+ #
81
+ # Git.new_branch("bug-123-fix-thing", "master")
82
+ def self.new_branch(new_branch_name, source_branch_name)
83
+ fetch
84
+ perform("branch --no-track #{new_branch_name} origin/#{source_branch_name}")
85
+ check_out new_branch_name
86
+ perform("push --set-upstream origin #{new_branch_name}")
87
+ end
88
+
89
+ # Public: Whether the Git index is clean (has no uncommited changes)
90
+ #
91
+ # Returns a Boolean
92
+ def self.clean?
93
+ # git status --short returns one line for any uncommited changes, if any
94
+ # e.g.,
95
+ # ?? untracked.txt
96
+ # D deleted.txt
97
+ # M modified.txt
98
+ cli.perform_quietly("git status --short").empty?
99
+ end
100
+
101
+ # Public: Perform the block if the Git index is clean
102
+ def self.if_clean(message=DEFAULT_DIRTY_MESSAGE)
103
+ if clean?
104
+ yield
105
+ else
106
+ alert_dirty_index message
107
+ end
108
+ end
109
+
110
+ # Public: Display the message and show the git status
111
+ def self.alert_dirty_index(message)
112
+ cli.say " "
113
+ cli.say message
114
+ cli.say " "
115
+ perform "status"
116
+ end
117
+
118
+ # Public: Merge the given remote branch into the current branch
119
+ def self.merge(branch_name)
120
+ Git.if_clean do
121
+ Git.fetch
122
+ perform "merge --no-ff origin/#{branch_name}"
123
+ raise MergeFailed unless Git.clean?
124
+ Git.push
125
+ end
126
+ end
127
+
128
+ # Public: Fetch the latest changes from GitHub
129
+ def self.fetch
130
+ perform_quietly "fetch --prune"
131
+ end
132
+
133
+ # Public: Push the current branch to GitHub
134
+ def self.push
135
+ if_clean do
136
+ perform "push origin #{current_branch}"
137
+ end
138
+ end
139
+
140
+ # Public: Pull the latest changes for the checked-out branch
141
+ def self.pull
142
+ if_clean do
143
+ perform "pull"
144
+ end
145
+ end
146
+
147
+ # Public: The list of branches on GitHub
148
+ #
149
+ # Returns an Array of Strings containing the branch names
150
+ def self.remote_branches
151
+ Git.fetch
152
+ raw = Git.perform_quietly "branch --remote"
153
+ all_branches = raw.split("\n").map do |raw_name|
154
+ # will come in as " origin/foo", we want just "foo"
155
+ raw_name.split("/").last
156
+ end
157
+
158
+ all_branches.uniq.sort
159
+ end
160
+
161
+ # Public: List of branches starting with the given string
162
+ #
163
+ # prefix - String to match branch names against
164
+ #
165
+ # Returns an Array of Strings containing the branch names
166
+ def self.branches_for(prefix)
167
+ remote_branches.select do |branch_name|
168
+ branch_name =~ /^#{prefix}/
169
+ end
170
+ end
171
+
172
+ def self.latest_branch_for(branch_prefix)
173
+ branches_for(branch_prefix).last || raise(NoBranchOfType, "No #{branch_prefix} branch")
174
+ end
175
+
176
+ # Public: The name of the current deployable branch
177
+ def self.deployable_branch
178
+ latest_branch_for(DEPLOYABLE_PREFIX)
179
+ end
180
+
181
+ # Public: The name of the current staging branch
182
+ def self.staging_branch
183
+ latest_branch_for(STAGING_PREFIX)
184
+ end
185
+
186
+ # Public: The name of the current QA-ready branch
187
+ def self.qaready_branch
188
+ latest_branch_for(QAREADY_PREFIX)
189
+ end
190
+
191
+ # Public: The list of releases which have been tagged
192
+ #
193
+ # Returns an Array of Strings containing the tag names
194
+ def self.release_tags
195
+ Git.perform_quietly("tag").split("\n").select do |tag|
196
+ tag =~ RELEASE_TAG_FILTER
197
+ end
198
+ end
199
+
200
+ # Public: Only the most recent release tags
201
+ #
202
+ # Returns an Array of Strings containing the tag names
203
+ def self.recent_release_tags
204
+ release_tags.last(RECENT_TAG_LIMIT)
205
+ end
206
+
207
+ # Public: Create a new tag with the given name
208
+ #
209
+ # tag_name - The name of the tag to create
210
+ def self.new_tag(tag_name)
211
+ perform "tag #{tag_name}"
212
+ push
213
+ perform "push --tag"
214
+ end
215
+
216
+ # Public: Delete the given branch
217
+ #
218
+ # branch_name - The name of the branch to delete
219
+ def self.delete_branch(branch_name)
220
+ perform "push origin :#{branch_name}"
221
+ perform "branch -D #{branch_name}"
222
+ end
223
+
224
+ # Public: Branches which have been merged into the given branch
225
+ #
226
+ # source_branch_name - The name of the branch to check against
227
+ # branches_to_ignore - An Array of branches to exclude from results
228
+ #
229
+ # Returns an Array of Strings
230
+ def self.stale_branches(source_branch_name="master", branches_to_ignore=[])
231
+ Git.fetch
232
+ command = "branch --remote --merged #{recent_sha(source_branch_name)} | grep -E -v '(#{stale_branches_to_ignore(branches_to_ignore).join("|")})'"
233
+ raw_result = Git.perform_quietly command
234
+ raw_result.split.map { |full_name| full_name.gsub("origin/", "") }
235
+ end
236
+
237
+ # Private: The SHA from 1 day ago for the given branch
238
+ #
239
+ # branch_name - The name of the branch to check
240
+ #
241
+ # Returns a String
242
+ def self.recent_sha(branch_name)
243
+ raw = perform_quietly "rev-list `git rev-parse remotes/origin/#{branch_name} --before=1.day.ago` --max-count=1"
244
+ raw.chomp
245
+ end
246
+ private_class_method :recent_sha
247
+
248
+ # Private: Branches to ignore when looking for stale branches
249
+ #
250
+ # Returns an Array of Strings
251
+ def self.stale_branches_to_ignore(additional_branches=[])
252
+ %w(HEAD master staging deployable) + Array(additional_branches)
253
+ end
254
+ private_class_method :stale_branches_to_ignore
255
+
256
+ # Exceptions
257
+ NotOnBranch = Class.new(StandardError)
258
+ CheckoutFailed = Class.new(StandardError)
259
+ MergeFailed = Class.new(StandardError)
260
+ NoBranchOfType = Class.new(StandardError)
261
+ end
262
+ end