<commit_before># rubocop: disable Rails/Output
module Gitlab
  # Checks if a set of migrations requires downtime or not.
  class EeCompatCheck
    DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
    EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
    CHECK_DIR = Rails.root.join('ee_compat_check')
    IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
    PLEASE_READ_THIS_BANNER = %Q{
      ============================================================
      ===================== PLEASE READ THIS =====================
      ============================================================
    }.freeze
    THANKS_FOR_READING_BANNER = %Q{
      ============================================================
      ==================== THANKS FOR READING ====================
      ============================================================\n
    }.freeze

    attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
    attr_reader :job_id, :failed_files

    def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
      @ee_repo_dir = CHECK_DIR.join('ee-repo')
      @patches_dir = CHECK_DIR.join('patches')
      @ce_branch = branch
      @ce_project_url = ce_project_url
      @ce_repo_url = "#{ce_project_url}.git"
      @job_id = job_id
    end

    def check
      ensure_patches_dir
      add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git")
      generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce')

      ensure_ee_repo
      Dir.chdir(ee_repo_dir) do
        step("In the #{ee_repo_dir} directory")

        add_remote('canonical-ee', EE_REPO_URL)

        status = catch(:halt_check) do
          ce_branch_compat_check!
          delete_ee_branches_locally!
          ee_branch_presence_check!

          step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}])
          generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee')
          ee_branch_compat_check!
        end

        delete_ee_branches_locally!

        if status.nil?
          true
        else
          false
        end
      end
    end

    private

    def add_remote(name, url)
      step(
        "Adding the #{name} remote (#{url})",
        %W[git remote add #{name} #{url}]
      )
    end

    def ensure_ee_repo
      if Dir.exist?(ee_repo_dir)
        step("#{ee_repo_dir} already exists")
      else
        step(
          "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
          %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
        )
      end
    end

    def ensure_patches_dir
      FileUtils.mkdir_p(patches_dir)
    end

    def generate_patch(branch:, patch_path:, remote:)
      FileUtils.rm(patch_path, force: true)

      find_merge_base_with_master(branch: branch, master_remote: remote)

      step(
        "Generating the patch against #{remote}/master in #{patch_path}",
        %W[git diff --binary #{remote}/master...#{branch}]
      ) do |output, status|
        throw(:halt_check, :ko) unless status.zero?

        File.write(patch_path, output)

        throw(:halt_check, :ko) unless File.exist?(patch_path)
      end
    end

    def ce_branch_compat_check!
      if check_patch(ce_patch_full_path, remote: 'canonical-ce').zero?
        puts applies_cleanly_msg(ce_branch)
        throw(:halt_check)
      end
    end

    def ee_branch_presence_check!
      _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}])

      if status.zero?
        @ee_branch_found = ee_branch_prefix
        return
      end

      _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}])

      if status.zero?
        @ee_branch_found = ee_branch_suffix
      else
        puts
        puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg

        throw(:halt_check, :ko)
      end
    end

    def ee_branch_compat_check!
      unless check_patch(ee_patch_full_path, remote: 'canonical-ee').zero?
        puts
        puts ee_branch_doesnt_apply_cleanly_msg

        throw(:halt_check, :ko)
      end

      puts
      puts applies_cleanly_msg(ee_branch_found)
    end

    def check_patch(patch_path, remote:)
      step("Checking out master", %w[git checkout master])
      step("Resetting to latest master", %W[git reset --hard #{remote}/master])
      step(
        "Checking if #{patch_path} applies cleanly to EE/master",
        # Don't use --check here because it can result in a 0-exit status even
        # though the patch doesn't apply cleanly, e.g.:
        #   > git apply --check --3way foo.patch
        #   error: patch failed: lib/gitlab/ee_compat_check.rb:74
        #   Falling back to three-way merge...
        #   Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts.
        #   > echo $?
        #   0
        %W[git apply --3way #{patch_path}]
      ) do |output, status|
        puts output
        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

          status = 0 if failed_files.empty?
        end

        command(%w[git reset --hard])
        status
      end
    end

    def delete_ee_branches_locally!
      command(%w[git checkout master])
      command(%W[git branch --delete --force #{ee_branch_prefix}])
      command(%W[git branch --delete --force #{ee_branch_suffix}])
    end

    def merge_base_found?(master_remote:, branch:)
      step(
        "Finding merge base with #{master_remote}/master",
        %W[git merge-base #{master_remote}/master #{branch}]
      ) do |output, status|
        if status.zero?
          puts "Merge base was found: #{output}"
          true
        end
      end
    end

    def find_merge_base_with_master(branch:, master_remote:)
      # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
      # In total we go (20 + 54 + 148 + 403 = 625) commits deeper
      depth = 20
      success =
        (3..6).any? do |factor|
          depth += Math.exp(factor).to_i
          # Repository is initially cloned with a depth of 20 so we need to fetch
          # deeper in the case the branch has more than 20 commits on top of master
          fetch(branch: branch, depth: depth, remote: 'origin')
          fetch(branch: 'master', depth: depth, remote: master_remote)

          merge_base_found?(master_remote: master_remote, branch: branch)
        end

      raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success
    end

    def fetch(branch:, depth:, remote: 'origin')
      step(
        "Fetching deeper...",
        %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/#{remote}/#{branch}]
      ) do |output, status|<commit_msg>Try to always find the merge base using `origin/#{branch}` instead of just `#{branch}`<commit_after># rubocop: disable Rails/Output
module Gitlab
  # Checks if a set of migrations requires downtime or not.
  class EeCompatCheck
    DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
    EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
    CHECK_DIR = Rails.root.join('ee_compat_check')
    IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
    PLEASE_READ_THIS_BANNER = %Q{
      ============================================================
      ===================== PLEASE READ THIS =====================
      ============================================================
    }.freeze
    THANKS_FOR_READING_BANNER = %Q{
      ============================================================
      ==================== THANKS FOR READING ====================
      ============================================================\n
    }.freeze

    attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
    attr_reader :job_id, :failed_files

    def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
      @ee_repo_dir = CHECK_DIR.join('ee-repo')
      @patches_dir = CHECK_DIR.join('patches')
      @ce_branch = branch
      @ce_project_url = ce_project_url
      @ce_repo_url = "#{ce_project_url}.git"
      @job_id = job_id
    end

    def check
      ensure_patches_dir
      add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git")
      generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce')

      ensure_ee_repo
      Dir.chdir(ee_repo_dir) do
        step("In the #{ee_repo_dir} directory")

        add_remote('canonical-ee', EE_REPO_URL)

        status = catch(:halt_check) do
          ce_branch_compat_check!
          delete_ee_branches_locally!
          ee_branch_presence_check!

          step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}])
          generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee')
          ee_branch_compat_check!
        end

        delete_ee_branches_locally!

        if status.nil?
          true
        else
          false
        end
      end
    end

    private

    def add_remote(name, url)
      step(
        "Adding the #{name} remote (#{url})",
        %W[git remote add #{name} #{url}]
      )
    end

    def ensure_ee_repo
      if Dir.exist?(ee_repo_dir)
        step("#{ee_repo_dir} already exists")
      else
        step(
          "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
          %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
        )
      end
    end

    def ensure_patches_dir
      FileUtils.mkdir_p(patches_dir)
    end

    def generate_patch(branch:, patch_path:, remote:)
      FileUtils.rm(patch_path, force: true)

      find_merge_base_with_master(branch: branch, master_remote: remote)

      step(
        "Generating the patch against #{remote}/master in #{patch_path}",
        %W[git diff --binary #{remote}/master...#{branch}]
      ) do |output, status|
        throw(:halt_check, :ko) unless status.zero?

        File.write(patch_path, output)

        throw(:halt_check, :ko) unless File.exist?(patch_path)
      end
    end

    def ce_branch_compat_check!
      if check_patch(ce_patch_full_path, remote: 'canonical-ce').zero?
        puts applies_cleanly_msg(ce_branch)
        throw(:halt_check)
      end
    end

    def ee_branch_presence_check!
      _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}])

      if status.zero?
        @ee_branch_found = ee_branch_prefix
        return
      end

      _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}])

      if status.zero?
        @ee_branch_found = ee_branch_suffix
      else
        puts
        puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg

        throw(:halt_check, :ko)
      end
    end

    def ee_branch_compat_check!
      unless check_patch(ee_patch_full_path, remote: 'canonical-ee').zero?
        puts
        puts ee_branch_doesnt_apply_cleanly_msg

        throw(:halt_check, :ko)
      end

      puts
      puts applies_cleanly_msg(ee_branch_found)
    end

    def check_patch(patch_path, remote:)
      step("Checking out master", %w[git checkout master])
      step("Resetting to latest master", %W[git reset --hard #{remote}/master])
      step(
        "Checking if #{patch_path} applies cleanly to EE/master",
        # Don't use --check here because it can result in a 0-exit status even
        # though the patch doesn't apply cleanly, e.g.:
        #   > git apply --check --3way foo.patch
        #   error: patch failed: lib/gitlab/ee_compat_check.rb:74
        #   Falling back to three-way merge...
        #   Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts.
        #   > echo $?
        #   0
        %W[git apply --3way #{patch_path}]
      ) do |output, status|
        puts output
        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

          status = 0 if failed_files.empty?
        end

        command(%w[git reset --hard])
        status
      end
    end

    def delete_ee_branches_locally!
      command(%w[git checkout master])
      command(%W[git branch --delete --force #{ee_branch_prefix}])
      command(%W[git branch --delete --force #{ee_branch_suffix}])
    end

    def merge_base_found?(master_remote:, branch:)
      step(
        "Finding merge base with #{master_remote}/master",
        %W[git merge-base #{master_remote}/master origin/#{branch}]
      ) do |output, status|
        if status.zero?
          puts "Merge base was found: #{output}"
          true
        end
      end
    end

    def find_merge_base_with_master(branch:, master_remote:)
      # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
      # In total we go (20 + 54 + 148 + 403 = 625) commits deeper
      depth = 20
      success =
        (3..6).any? do |factor|
          depth += Math.exp(factor).to_i
          # Repository is initially cloned with a depth of 20 so we need to fetch
          # deeper in the case the branch has more than 20 commits on top of master
          fetch(branch: branch, depth: depth, remote: 'origin')
          fetch(branch: 'master', depth: depth, remote: master_remote)

          merge_base_found?(master_remote: master_remote, branch: branch)
        end

      raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success
    end

    def fetch(branch:, depth:, remote: 'origin')
      step(
        "Fetching deeper...",
        %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/#{remote}/#{branch}]
      ) do |output, status|
{'fraction_non_alphanumeric': 0.1482199750657986, 'fraction_numerical': 0.005263886964953595, 'mean_word_length': 2.623337515683814, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7222578', 'n_tokens_mistral': 4647, 'n_tokens_neox': 4440, 'n_words': 1051}