# rubocop: disable Rails/Output module Gitlab # Checks if a set of migrations requires downtime or not. class EeCompatCheck CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze CHECK_DIR = Rails.root.join('ee_compat_check') MAX_FETCH_DEPTH = 500 IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch def initialize(branch:, ce_repo: CE_REPO) @repo_dir = CHECK_DIR.join('repo') @patches_dir = CHECK_DIR.join('patches') @ce_branch = branch @ce_repo = ce_repo end def check ensure_ee_repo ensure_patches_dir generate_patch(ce_branch, ce_patch_full_path) Dir.chdir(repo_dir) do step("In the #{repo_dir} directory") status = catch(:halt_check) do ce_branch_compat_check! delete_ee_branch_locally! ee_branch_presence_check! ee_branch_compat_check! end delete_ee_branch_locally! if status.nil? true else false end end end private def ensure_ee_repo if Dir.exist?(repo_dir) step("#{repo_dir} already exists") else cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}] step("Cloning #{EE_REPO} into #{repo_dir}", cmd) end end def ensure_patches_dir FileUtils.mkdir_p(patches_dir) end def generate_patch(branch, patch_path) FileUtils.rm(patch_path, force: true) depth = 0 loop do depth += 50 cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master] Gitlab::Popen.popen(cmd) _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD]) raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH break if status.zero? end step("Generating the patch against master in #{patch_path}") output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) throw(:halt_check, :ko) unless status.zero? File.write(patch_path, output) throw(:halt_check, :ko) unless File.exist?(patch_path) end def ce_branch_compat_check! if check_patch(ce_patch_full_path).zero? puts applies_cleanly_msg(ce_branch) throw(:halt_check) end end def ee_branch_presence_check! status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}]) unless status.zero? puts puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg throw(:halt_check, :ko) end end def ee_branch_compat_check! step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD]) generate_patch(ee_branch, ee_patch_full_path) unless check_patch(ee_patch_full_path).zero? puts puts ee_branch_doesnt_apply_cleanly_msg throw(:halt_check, :ko) end puts puts applies_cleanly_msg(ee_branch) end def check_patch(patch_path) step("Checking out master", %w[git checkout master]) step("Reseting to latest master", %w[git reset --hard origin/master]) step("Checking if #{patch_path} applies cleanly to EE/master") output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}]) unless status.zero? failed_files = output.lines.reduce([]) do |memo, line| if line.start_with?('error: patch failed:') file = line.sub(/\Aerror: patch failed: /, '') memo << file unless file =~ IGNORED_FILES_REGEX end memo end if failed_files.empty? status = 0 else puts "\nConflicting files:" failed_files.each do |file| puts " - #{file}" end end end status end def delete_ee_branch_locally! command(%w[git checkout master]) step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}]) end def ce_patch_name @ce_patch_name ||= patch_name_from_branch(ce_branch) end def ce_patch_full_path @ce_patch_full_path ||= patches_dir.join(ce_patch_name) end def ee_branch @ee_branch ||= "#{ce_branch}-ee" end def ee_patch_name @ee_patch_name ||= patch_name_from_branch(ee_branch) end def ee_patch_full_path @ee_patch_full_path ||= patches_dir.join(ee_patch_name) end def patch_name_from_branch(branch_name) branch_name.parameterize << '.patch' end def step(desc, cmd = nil) puts "\n=> #{desc}\n" if cmd start = Time.now puts "\n$ #{cmd.join(' ')}" status = command(cmd) puts "\nFinished in #{Time.now - start} seconds" status end end def command(cmd) output, status = Gitlab::Popen.popen(cmd) puts output status end def applies_cleanly_msg(branch) <<-MSG.strip_heredoc ================================================================= 🎉 Congratulations!! 🎉 The #{branch} branch applies cleanly to EE/master! Much ❤️!! =================================================================\n MSG end def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg <<-MSG.strip_heredoc ================================================================= 💥 Oh no! 💥 The #{ce_branch} branch does not apply cleanly to the current EE/master, and no #{ee_branch} branch was found in the EE repository. Please create a #{ee_branch} branch that includes changes from #{ce_branch} but also specific changes than can be applied cleanly to EE/master. There are different ways to create such branch: 1. Create a new branch based on the CE branch and rebase it on top of EE/master # In the EE repo $ git fetch #{ce_repo} #{ce_branch} $ git checkout -b #{ee_branch} FETCH_HEAD # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit # before rebasing to limit the conflicts-resolving steps during the rebase $ git fetch origin $ git rebase origin/master At this point you will likely have conflicts. Solve them, and continue/finish the rebase. You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE". 2. Create a new branch from master and cherry-pick your CE commits # In the EE repo $ git fetch origin $ git checkout -b #{ee_branch} origin/master $ git fetch #{ce_repo} #{ce_branch} $ git cherry-pick SHA # Repeat for all the commits you want to pick You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit. Don't forget to push your branch to #{EE_REPO}: # In the EE repo $ git push origin #{ee_branch} You can then retry this failed build, and hopefully it should pass. Stay 💪 ! =================================================================\n MSG end def ee_branch_doesnt_apply_cleanly_msg <<-MSG.strip_heredoc ================================================================= 💥 Oh no! 💥 The #{ce_branch} does not apply cleanly to the current EE/master, and even though a #{ee_branch} branch exists in the EE repository, it does not apply cleanly either to EE/master! Please update the #{ee_branch}, push it again to #{EE_REPO}, and retry this build. Stay 💪 ! =================================================================\n MSG end end end