octopolo 0.0.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.
- checksums.yaml +15 -0
- data/.gitignore +21 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Guardfile +5 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +55 -0
- data/Rakefile +38 -0
- data/bash_completion.sh +13 -0
- data/bin/octopolo +21 -0
- data/bin/op +21 -0
- data/lib/octopolo.rb +15 -0
- data/lib/octopolo/changelog.rb +27 -0
- data/lib/octopolo/cli.rb +210 -0
- data/lib/octopolo/commands/accept_pull.rb +8 -0
- data/lib/octopolo/commands/compare_release.rb +9 -0
- data/lib/octopolo/commands/deployable.rb +8 -0
- data/lib/octopolo/commands/github_auth.rb +5 -0
- data/lib/octopolo/commands/new_branch.rb +9 -0
- data/lib/octopolo/commands/new_deployable.rb +8 -0
- data/lib/octopolo/commands/new_staging.rb +8 -0
- data/lib/octopolo/commands/octopolo_setup.rb +5 -0
- data/lib/octopolo/commands/pivotal_auth.rb +5 -0
- data/lib/octopolo/commands/pull_request.rb +13 -0
- data/lib/octopolo/commands/signoff.rb +10 -0
- data/lib/octopolo/commands/stage_up.rb +8 -0
- data/lib/octopolo/commands/stale_branches.rb +11 -0
- data/lib/octopolo/commands/sync_branch.rb +11 -0
- data/lib/octopolo/commands/tag_release.rb +13 -0
- data/lib/octopolo/config.rb +146 -0
- data/lib/octopolo/convenience_wrappers.rb +46 -0
- data/lib/octopolo/dated_branch_creator.rb +81 -0
- data/lib/octopolo/git.rb +262 -0
- data/lib/octopolo/github.rb +95 -0
- data/lib/octopolo/github/commit.rb +45 -0
- data/lib/octopolo/github/pull_request.rb +126 -0
- data/lib/octopolo/github/pull_request_creator.rb +127 -0
- data/lib/octopolo/github/user.rb +40 -0
- data/lib/octopolo/jira/story_commenter.rb +26 -0
- data/lib/octopolo/pivotal.rb +44 -0
- data/lib/octopolo/pivotal/story_commenter.rb +19 -0
- data/lib/octopolo/pull_request_merger.rb +99 -0
- data/lib/octopolo/renderer.rb +37 -0
- data/lib/octopolo/reports.rb +18 -0
- data/lib/octopolo/scripts.rb +23 -0
- data/lib/octopolo/scripts/accept_pull.rb +67 -0
- data/lib/octopolo/scripts/compare_release.rb +52 -0
- data/lib/octopolo/scripts/deployable.rb +27 -0
- data/lib/octopolo/scripts/github_auth.rb +87 -0
- data/lib/octopolo/scripts/new_branch.rb +34 -0
- data/lib/octopolo/scripts/new_deployable.rb +14 -0
- data/lib/octopolo/scripts/new_staging.rb +15 -0
- data/lib/octopolo/scripts/octopolo_setup.rb +55 -0
- data/lib/octopolo/scripts/pivotal_auth.rb +44 -0
- data/lib/octopolo/scripts/pull_request.rb +127 -0
- data/lib/octopolo/scripts/signoff.rb +85 -0
- data/lib/octopolo/scripts/stage_up.rb +26 -0
- data/lib/octopolo/scripts/stale_branches.rb +54 -0
- data/lib/octopolo/scripts/sync_branch.rb +37 -0
- data/lib/octopolo/scripts/tag_release.rb +70 -0
- data/lib/octopolo/templates/pull_request_body.erb +24 -0
- data/lib/octopolo/user_config.rb +112 -0
- data/lib/octopolo/version.rb +3 -0
- data/lib/octopolo/week.rb +130 -0
- data/octopolo.gemspec +31 -0
- data/spec/.DS_Store +0 -0
- data/spec/octopolo/cli_spec.rb +310 -0
- data/spec/octopolo/config_spec.rb +344 -0
- data/spec/octopolo/convenience_wrappers_spec.rb +80 -0
- data/spec/octopolo/dated_branch_creator_spec.rb +143 -0
- data/spec/octopolo/git_spec.rb +419 -0
- data/spec/octopolo/github/commit_spec.rb +59 -0
- data/spec/octopolo/github/pull_request_creator_spec.rb +174 -0
- data/spec/octopolo/github/pull_request_spec.rb +291 -0
- data/spec/octopolo/github/user_spec.rb +65 -0
- data/spec/octopolo/github_spec.rb +169 -0
- data/spec/octopolo/jira/stor_commenter_spec.rb +30 -0
- data/spec/octopolo/pivotal/story_commenter_spec.rb +34 -0
- data/spec/octopolo/pivotal_spec.rb +61 -0
- data/spec/octopolo/pull_request_merger_spec.rb +144 -0
- data/spec/octopolo/renderer_spec.rb +35 -0
- data/spec/octopolo/scripts/accept_pull_spec.rb +76 -0
- data/spec/octopolo/scripts/compare_release_spec.rb +115 -0
- data/spec/octopolo/scripts/deployable_spec.rb +52 -0
- data/spec/octopolo/scripts/github_auth_spec.rb +156 -0
- data/spec/octopolo/scripts/new_branch_spec.rb +41 -0
- data/spec/octopolo/scripts/new_deployable_spec.rb +18 -0
- data/spec/octopolo/scripts/new_staging_spec.rb +18 -0
- data/spec/octopolo/scripts/octopolo_setup_spec.rb +120 -0
- data/spec/octopolo/scripts/pivotal_auth_spec.rb +77 -0
- data/spec/octopolo/scripts/pull_request_spec.rb +217 -0
- data/spec/octopolo/scripts/signoff_spec.rb +139 -0
- data/spec/octopolo/scripts/stage_up_spec.rb +52 -0
- data/spec/octopolo/scripts/stale_branches_spec.rb +81 -0
- data/spec/octopolo/scripts/sync_branch_spec.rb +57 -0
- data/spec/octopolo/scripts/tag_release_spec.rb +108 -0
- data/spec/octopolo/user_config_spec.rb +167 -0
- data/spec/octopolo_spec.rb +7 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/engine_yard.cache +0 -0
- data/spec/support/sample_octopolo.yml +2 -0
- data/spec/support/sample_user.yml +2 -0
- data/templates/lib.erb +23 -0
- data/templates/script.erb +7 -0
- data/templates/spec.erb +29 -0
- 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,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,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,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
|
+
|
data/lib/octopolo/git.rb
ADDED
|
@@ -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
|