mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-20 17:51:44 +00:00
372 lines
9.6 KiB
Ruby
Executable File
372 lines
9.6 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
# frozen_string_literal: true
|
|
|
|
require 'active_support/core_ext/object/to_query'
|
|
require 'optparse'
|
|
require 'open3'
|
|
require 'rainbow/refinement'
|
|
require 'tty-prompt'
|
|
using Rainbow
|
|
|
|
class Backport
|
|
SECURITY_REPO_URLS = {
|
|
ssh: 'git@gitlab.com:gitlab-org/security/gitlab.git',
|
|
http: 'https://gitlab.com/gitlab-org/security/gitlab.git'
|
|
}.freeze
|
|
|
|
DEFAULT_OPTIONS = {
|
|
base: {
|
|
version: nil, branch: nil, sha: nil, merge_request: false, stable_branch_suffix: 'stable'
|
|
},
|
|
security: {
|
|
branch_prefix: 'security',
|
|
new_merge_request_url: 'https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/new',
|
|
merge_request_template: 'Security Fix'
|
|
},
|
|
bugfix: {
|
|
branch_prefix: 'backport',
|
|
new_merge_request_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
|
|
merge_request_template: 'Stable Branch'
|
|
}
|
|
}.freeze
|
|
|
|
BACKPORT_TYPE_CHOICES = [
|
|
{ name: 'Security Fix', value: :security }.freeze,
|
|
{ name: 'Bug Fix', value: :bugfix }.freeze
|
|
].freeze
|
|
|
|
def initialize
|
|
@prompt = TTY::Prompt.new(help_color: :cyan)
|
|
@options = build_options
|
|
end
|
|
|
|
attr_reader :prompt
|
|
|
|
def create!
|
|
correct_repository_check!
|
|
|
|
return dry_run if dry_run?
|
|
|
|
pick_commits
|
|
push_commits
|
|
end
|
|
|
|
private
|
|
|
|
def correct_repository_check!
|
|
if security_backport? && !pushing_to_security_remote?
|
|
abort('⛔️ Can only push security backports to the security repository ⛔️'.red)
|
|
elsif !security_backport? && pushing_to_security_remote?
|
|
abort('⛔️ Bugfixes should be pushed to the canonical repository ⛔️'.red)
|
|
end
|
|
end
|
|
|
|
def pushing_to_security_remote?
|
|
@options[:remote] == security_remote_name
|
|
end
|
|
|
|
def build_options
|
|
options = DEFAULT_OPTIONS[:base].dup
|
|
parse_options(options)
|
|
|
|
options[:sha] ||= git_head_sha
|
|
options[:branch] ||= git_current_branch
|
|
options[:remote] ||= select_git_remote
|
|
options[:version] ||= select_version
|
|
|
|
options.merge(DEFAULT_OPTIONS[confirmed_backport_type!])
|
|
end
|
|
|
|
def confirmed_backport_type!
|
|
backport_type = prompt.select('⚠️ What type of fix are you backporting? ⚠️'.red, BACKPORT_TYPE_CHOICES)
|
|
|
|
@security_backport = backport_type == :security
|
|
|
|
backport_type
|
|
end
|
|
|
|
def security_backport?
|
|
@security_backport
|
|
end
|
|
|
|
def add_security_remote
|
|
puts "⚠️ You do not have the security remote configured ⚠️".red
|
|
|
|
return attempt_to_add_security_remote if prompt.yes?('Would you like me to add the remote for you now?')
|
|
|
|
print_security_remote_config_instructions_and_exit
|
|
end
|
|
|
|
def attempt_to_add_security_remote
|
|
security_repo_url = prompt.select('Which remote format do you normally use?', git_remote_style_choices)
|
|
stdin, stdout, stderr, wait_thr = Open3.popen3("git remote add security #{security_repo_url}")
|
|
|
|
if wait_thr.value.success? # rubocop:disable Cop/LineBreakAroundConditionalBlock -- https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/258
|
|
@security_remote_name = 'security'
|
|
puts '✅ Successfully created `security` remote'.white
|
|
else
|
|
puts "⛔️ Could not create `security` remote ⛔️".red
|
|
puts ('-' * 80).red
|
|
puts "\n#{stderr.read}".red
|
|
puts ('-' * 80).red
|
|
|
|
print_security_remote_config_instructions_and_exit
|
|
end
|
|
ensure
|
|
stdin.close
|
|
stdout.close
|
|
stderr.close
|
|
end
|
|
|
|
def print_security_remote_config_instructions_and_exit
|
|
puts "Please add the gitlab security remote to git with:".white
|
|
puts "git remote add security #{SECURITY_REPO_URLS[:ssh]}".cyan
|
|
puts "or".white
|
|
puts "git remote add security #{SECURITY_REPO_URLS[:http]}".cyan
|
|
puts "and then try again".white
|
|
exit 1
|
|
end
|
|
|
|
def parse_options(options)
|
|
parser = OptionParser.new do |opts|
|
|
opts.banner = <<~BANNER
|
|
Usage: #{$PROGRAM_NAME} [options]
|
|
|
|
This tool requires confirmation for the backport type and will prompt
|
|
for the remote and version unless specified.
|
|
|
|
BANNER
|
|
|
|
opts.on('-v', '--version 10.0', 'Version to target (opens prompt if not passed)') do |version|
|
|
options[:version] = version&.tr('.', '-')
|
|
end
|
|
|
|
opts.on('-r', '--remote dev', "Git remote name of repository (opens prompt if not passed)") do |remote|
|
|
options[:remote] = remote
|
|
end
|
|
|
|
opts.on('-b', '--branch branch-name', 'Original branch name (optional, defaults to current branch)') do |branch|
|
|
options[:branch] = branch
|
|
end
|
|
|
|
opts.on('-s', '--sha abcd', 'SHA or SHA range to cherry pick (optional, defaults to HEAD SHA)') do |sha|
|
|
options[:sha] = sha
|
|
end
|
|
|
|
opts.on('--mr', '--merge-request', 'Create a Merge Request targeting the stable branch') do
|
|
options[:merge_request] = true
|
|
end
|
|
|
|
opts.on('-d', '--dry-run', 'Display the Git commands this script will run without calling them') do
|
|
options[:try] = true
|
|
end
|
|
|
|
opts.on('-h', '--help', 'Displays Help') do
|
|
puts opts
|
|
|
|
exit
|
|
end
|
|
end
|
|
|
|
parser.parse!
|
|
end
|
|
|
|
def git_head_sha
|
|
`git rev-parse HEAD`.strip
|
|
end
|
|
|
|
def git_current_branch
|
|
`git rev-parse --abbrev-ref HEAD`.strip
|
|
end
|
|
|
|
def select_git_remote
|
|
prompt.select("Which remote do you want to push to?", remote_choices)
|
|
end
|
|
|
|
def git_remotes
|
|
@git_remotes ||= `git remote -v`.strip.split("\n").each_with_object({}) do |line, output|
|
|
name, url, _type = line.split(/\s+/)
|
|
output[name] = url
|
|
end
|
|
end
|
|
|
|
def security_remote_name
|
|
return @security_remote_name if defined?(@security_remote_name)
|
|
|
|
@security_remote_name = git_remotes.find { |_k, url| SECURITY_REPO_URLS.value?(url) }&.first
|
|
add_security_remote if @security_remote_name.nil?
|
|
@security_remote_name
|
|
end
|
|
|
|
# Sorts git remotes so the security remote is first in the list
|
|
def remote_choices
|
|
[security_remote_name] + (git_remotes.keys - [security_remote_name])
|
|
end
|
|
|
|
def git_remote_style_choices
|
|
SECURITY_REPO_URLS.map { |style, url| { name: "#{style}: #{url}", value: url } }
|
|
end
|
|
|
|
def select_version
|
|
prompt.select('Which version are you targeting?', version_choices)
|
|
end
|
|
|
|
def current_version
|
|
version_filepath = File.join(File.dirname($PROGRAM_NAME), '../VERSION')
|
|
full_version = File.read(version_filepath)
|
|
major, minor, _rest = full_version.split('.')
|
|
[major.to_i, minor.to_i]
|
|
end
|
|
|
|
def version_choices
|
|
major, minor = current_version
|
|
|
|
Array.new(3) do
|
|
minor -= 1
|
|
|
|
if minor < 0
|
|
major -= 1
|
|
minor = 11
|
|
end
|
|
|
|
{ name: "#{major}.#{minor}", value: "#{major}-#{minor}" }
|
|
end
|
|
end
|
|
|
|
def dry_run?
|
|
@options[:try] == true
|
|
end
|
|
|
|
def dry_run
|
|
puts "\nGit commands:".blue
|
|
puts git_commands.join("\n")
|
|
puts "\nMerge request URL:".blue
|
|
puts new_merge_request_url
|
|
end
|
|
|
|
def pick_commits
|
|
cmd = git_pick_commands.join(' && ')
|
|
stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
|
|
|
|
puts stdout.read.green
|
|
puts stderr.read.red
|
|
|
|
unless wait_thr.value.success? # rubocop:disable Cop/LineBreakAroundConditionalBlock -- https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/merge_requests/258
|
|
puts <<~MSG
|
|
It looks like cherry pick failed!
|
|
Open a new terminal and fix the conflicts.
|
|
Once fixed run `git cherry-pick --continue`
|
|
|
|
After you are done, return here and continue. (Press n to cancel)
|
|
|
|
Ready to continue? (Y/n)
|
|
MSG
|
|
|
|
unless ['', 'Y', 'y'].include?(gets.chomp)
|
|
puts "\nRemaining git commands:".blue
|
|
puts 'git cherry-pick --continue'
|
|
puts git_push_commands.join("\n")
|
|
exit 1
|
|
end
|
|
end
|
|
ensure
|
|
stdin.close
|
|
stdout.close
|
|
stderr.close
|
|
end
|
|
|
|
def push_commits
|
|
cmd = git_push_commands.join(' && ')
|
|
stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
|
|
|
|
puts stdout.read.green
|
|
puts stderr.read.red
|
|
|
|
puts new_merge_request_url.blue if wait_thr.value.success? && !@options[:merge_request]
|
|
ensure
|
|
stdin.close
|
|
stdout.close
|
|
stderr.close
|
|
end
|
|
|
|
def git_pick_commands
|
|
[
|
|
fetch_stable_branch,
|
|
create_backport_branch,
|
|
cherry_pick_commit
|
|
]
|
|
end
|
|
|
|
def git_push_commands
|
|
[
|
|
push_to_remote,
|
|
checkout_original_branch
|
|
]
|
|
end
|
|
|
|
def git_commands
|
|
git_pick_commands + git_push_commands
|
|
end
|
|
|
|
def fetch_stable_branch
|
|
"git fetch #{@options[:remote]} #{stable_branch}"
|
|
end
|
|
|
|
def create_backport_branch
|
|
"git checkout -B #{source_branch} #{@options[:remote]}/#{stable_branch} --no-track"
|
|
end
|
|
|
|
def cherry_pick_commit
|
|
"git cherry-pick #{@options[:sha]}"
|
|
end
|
|
|
|
def push_to_remote
|
|
[
|
|
"git push #{@options[:remote]} #{source_branch} --no-verify",
|
|
*merge_request_push_options
|
|
].join(' ')
|
|
end
|
|
|
|
def checkout_original_branch
|
|
"git checkout #{@options[:branch]}"
|
|
end
|
|
|
|
def gitlab_params
|
|
{
|
|
issuable_template: @options[:merge_request_template],
|
|
merge_request: {
|
|
source_branch: source_branch,
|
|
target_branch: stable_branch
|
|
}
|
|
}
|
|
end
|
|
|
|
def merge_request_push_options
|
|
return [] unless @options[:merge_request]
|
|
|
|
[
|
|
"-o mr.create",
|
|
"-o mr.target='#{stable_branch}'",
|
|
"-o mr.description='Please apply `#{@options[:merge_request_template]}` template.'",
|
|
"-o mr.milestone='#{@options[:version].tr('-', '.')}'"
|
|
]
|
|
end
|
|
|
|
def source_branch
|
|
branch = "#{@options[:branch]}-#{@options[:version]}"
|
|
branch = "#{@options[:branch_prefix]}-#{branch}" unless branch.start_with?("#{@options[:branch_prefix]}-")
|
|
branch
|
|
end
|
|
|
|
def stable_branch
|
|
"#{@options[:version]}-#{@options[:stable_branch_suffix]}-ee"
|
|
end
|
|
|
|
def new_merge_request_url
|
|
"#{@options[:new_merge_request_url]}?#{gitlab_params.to_query}"
|
|
end
|
|
end
|
|
|
|
Backport.new.create!
|