ManageIQ/multi_repo

View on GitHub
lib/multi_repo/helpers/git_mirror.rb

Summary

Maintainability
B
6 hrs
Test Coverage
module MultiRepo::Helpers
  class GitMirror
    def initialize
      require "colorize"
      require "config"
      require "fileutils"

      @errors_occurred = false
    end

    def settings
      @settings ||= Config.load_files(MultiRepo.config_dir.join("settings.yml").to_s, MultiRepo.config_dir.join("settings.local.yml").to_s)
    end

    def mirror_all
      settings.git_mirror.repos_to_mirror.keys.each { |repo| mirror(repo) }
      !@errors_occurred
    end

    def mirror(repo)
      repo = repo.to_s
      options = default_repo_options.dup.merge!(settings.git_mirror.repos_to_mirror[repo].to_h)
      with_repo(repo, options) do
        send("mirror_#{options.remote_source}_repo", repo)
      end
      !@errors_occurred
    end

    private

    def default_repo_options
      Config::Options.new(:remote_source => :upstream)
    end

    def backup_remote_defined?
      !!settings.git_mirror.remotes.backup
    end

    def mirror_branches_for(repo)
      settings.git_mirror.branch_mirror_defaults.to_h.merge(settings.git_mirror.branch_mirror_overrides[repo].to_h || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v }
    end

    def mirror_branches(repo, source_remote, dest_remote)
      mirror_branches_for(repo).each do |source_name, dest_name|
        sync_branch(source_remote, source_name, dest_remote, dest_name)
      end
    end

    def mirror_upstream_repo(repo)
      mirror_remote_refs(repo, "upstream", "downstream")
      mirror_branches(repo, "upstream", "downstream")
      mirror_remote_refs(repo, "downstream", "backup") if backup_remote_defined?
    end

    def mirror_downstream_repo(repo)
      mirror_branches(repo, "downstream", "downstream")
      mirror_remote_refs(repo, "downstream", "backup") if backup_remote_defined?
    end

    def dry_run?
      return @dry_run if defined?(@dry_run)
      @dry_run = ARGV.include?("--dry-run")
    end

    def downstream_repo_name(repo, options)
      options.downstream_repo_name || repo.sub(/^manageiq/, settings.git_mirror.productization_name)
    end

    def system(*args)
      puts "+ #{"dry_run: " if dry_run?}#{args.join(" ")}"
      return true if dry_run?

      args << {} unless args.last.is_a?(Hash)
      args.last[[:out, :err]] = ["/tmp/mirror_helper_out", "w"]

      super.tap do |result|
        unless result
          @errors_occurred = true
          STDERR.puts "!!! An error has occurred:\n#{File.read("/tmp/mirror_helper_out")}".bold.red
        end
      end
    end

    def with_repo(repo, options)
      repo_name = downstream_repo_name(repo, options)
      puts "\n==== Mirroring #{repo_name} ====".bold.cyan

      working_dir = settings.git_mirror.working_directory
      FileUtils.mkdir_p(working_dir)

      path = "#{working_dir}/#{repo_name}"
      clone_repo(repo, repo_name, path, options.remote_source) unless File.directory?(path)

      Dir.chdir(path) do
        puts "\n==== Fetching for #{repo_name} ====".bold.green
        # Enforce an order for remote fetching to ensure that moved
        #   tags prefer what is on upstream
        system("git fetch backup --prune --tags") if backup_remote_defined? && remote_exists?("backup")
        system("git fetch downstream --prune --tags")
        system("git fetch upstream --prune --tags") if [:red_hat_cloudforms, :upstream].include?(options.remote_source)

        yield
      end

      puts
    end

    def clone_repo(upstream_repo, downstream_repo, path, remote_source)
      upstream_remote = settings.git_mirror.remotes[remote_source]
      raise "remote '#{remote_source}'' not found in settings" if upstream_remote.nil?

      system("git clone #{upstream_remote}/#{upstream_repo}.git #{path} -o upstream")
      Dir.chdir(path) do
        unless remote_exists?("downstream")
          downstream_remote = settings.git_mirror.remotes.downstream
          raise "remote 'downstream' not found in settings" if downstream_remote.nil?

          system("git remote add downstream #{downstream_remote}/#{downstream_repo}.git")
        end
        if backup_remote_defined? && !remote_exists?("backup")
          backup_remote = settings.git_mirror.remotes.backup
          system("git remote add backup #{backup_remote}/#{downstream_repo}.git")
        end
      end
    end

    def remote_refs(repo, remote)
      return unless remote_exists?(remote)

      `git ls-remote #{remote} | grep "heads"`.split("\n").collect do |line|
        branch = line.split("/").last
        next if remote == "upstream" && !upstream_branch?(repo, branch)
        "#{remote}/#{branch}:refs/heads/#{branch}"
      end.compact.join(" ")
    end

    def remote_exists?(remote)
      `git ls-remote #{remote} --exit-code 2>/dev/null`
      $? == 0
    end

    def upstream_branch?(repo, branch)
      (mirror_branches_for(repo).keys.collect(&:to_s) + ["master"]).include?(branch)
    end

    def remote_branch?(branch)
      !`git branch -r | grep "\\b#{branch}\\b"`.strip.empty?
    end

    def sync_branch(source_remote, source_name, dest_remote, dest_name)
      return unless dest_remote && dest_name

      source_fq_name = "#{source_remote}/#{source_name}"
      dest_fq_name   = "#{dest_remote}/#{dest_name}"

      puts "\n==== Syncing #{source_name} to #{dest_name} ====".bold.green
      unless remote_branch?(source_fq_name)
        puts "! Skipping sync of #{source_name} to #{dest_name} since #{source_fq_name} branch does not exist".yellow
        return
      end

      start_point = remote_branch?(dest_fq_name) ? dest_fq_name : source_fq_name
      system("git rebase --abort || true") # `git rebase --abort` will exit non-zero if there's nothing to abort
      system("git reset --hard")

      success =
        system("git checkout -B #{dest_name} #{start_point}") &&
        system("git pull --no-rebase #{source_remote} #{source_name}") &&
        system("git push -f #{dest_remote} #{dest_name}")

      if backup_remote_defined?
        if success && remote_exists?("backup")
          success = system("git push -f backup #{dest_name}")
        else
          puts "! Skipping sync of #{source_name} to backup/#{dest_name} since backup remote does not exist".yellow
        end
      end

      success
    end

    def mirror_remote_refs(repo, source_remote, dest_remote)
      puts "\n==== Mirroring #{source_remote} to #{dest_remote} ====".bold.green
      unless remote_exists?(dest_remote)
        puts "! Skipping mirror of #{source_remote} to #{dest_remote} since #{dest_remote} does not exist".yellow
        return
      end

      refs = remote_refs(repo, source_remote)
      if refs.to_s.strip.empty?
        puts "! Skipping mirror of #{source_remote} to #{dest_remote} since there are no refs to mirror".yellow
        return
      end

      system("git push #{dest_remote} #{refs}") &&
        system("git push -f #{dest_remote} --tags")
    end
  end
end