<commit_before># Implements AbstractRepository for Git repositories
# It implements the following paradigm:
#   1. Repositories are created by using ???
#   2. Existing repositories are opened by using either ???
class GitRepository < Repository::AbstractRepository

  DUMMY_FILE_NAME = '.gitkeep'.freeze

  # Constructor: Connects to an existing Git
  # repository, using Ruby bindings; Note: A repository has to be
  # created using GitRepository.create(), if it is not yet existent
  def initialize(connect_string)

    # Check if configuration is in order
    if MarkusConfigurator.markus_config_repository_admin?.nil?
      raise ConfigurationError.new("Required config 'IS_REPOSITORY_ADMIN' not set")
    end
    if MarkusConfigurator.markus_config_repository_storage.nil?
      raise ConfigurationError.new("Required config 'REPOSITORY_STORAGE' not set")
    end
    if MarkusConfigurator.markus_config_repository_permission_file.nil?
      raise ConfigurationError.new("Required config 'REPOSITORY_PERMISSION_FILE' not set")
    end
    begin
      super(connect_string) # dummy call to super
    rescue NotImplementedError; end
    @repos_path = connect_string
    @closed = false
    if GitRepository.repository_exists?(@repos_path)
      begin
        @repos = Rugged::Repository.new(@repos_path)
        # make sure working directory is up-to-date
        @repos.fetch('origin')
        begin
          @repos.reset('master', :hard) # TODO this shouldn't be necessary, but something is messing up the repo.
        rescue Rugged::ReferenceError   # It seems the master branch might not be correctly setup at first.
        end
        @repos.reset('origin/master', :hard) # align to whatever is in origin/master
      rescue StandardError # TODO this shouldn't be necessary. It catches the case when a local repo is corrupted,
                           #      we need to prevent the corruption instead/as well
        GitRepository.reclone_repo(connect_string)
      end
    else
      GitRepository.reclone_repo(connect_string)
    end
  end

  def self.reclone_repo(connect_string)
    repo_path, _sep, repo_name = connect_string.rpartition(File::SEPARATOR)
    bare_path = File.join(repo_path, 'bare', "#{repo_name}.git")
    raise "Cannot reclone from non-existant bare repository #{bare_path}" unless Dir.exist?(bare_path)
    if Dir.exist?(connect_string)
      bad_repo_path = "#{connect_string}.bad"
      FileUtils.rm_rf(bad_repo_path)
      FileUtils.mv(connect_string, bad_repo_path)
    end
    @repos = Rugged::Repository.clone_at(bare_path, connect_string)
    m_logger = MarkusLogger.instance
    m_logger.log "Recloned corrupted or missing git repo: #{connect_string}"
  rescue StandardError
    msg = "Failed to clone corrupted or missing git repo: #{connect_string}"
    m_logger = MarkusLogger.instance
    m_logger.log msg
    raise
  end

  def self.do_commit(repo, author, message)
    index = repo.index
    commit_tree = index.write_tree(repo)
    index.write
    commit_author = {email: '<EMAIL>', name: author, time: Time.now}
    commit_options = {
        author: commit_author,
        committer: commit_author,
        message: message,
        tree: commit_tree,
        parents: repo.empty? ? [] : [repo.head.target].compact, # compact if target returns nil (suggested upstream)
        update_ref: 'HEAD'
    }
    Rugged::Commit.create(repo, commit_options)
  end

  def self.do_commit_and_push(repo, author, message)
    GitRepository.do_commit(repo, author, message)
    repo.push('origin', ['refs/heads/master'])
  end

  # Static method: Creates a new Git repository at
  # location 'connect_string'
  def self.create(connect_string, with_hooks: true)
    if GitRepository.repository_exists?(connect_string)
      raise RepositoryCollision.new("There is already a repository at #{connect_string}")
    end
    if File.exists?(connect_string)
      raise IOError.new("Could not create a repository at #{connect_string}: some directory with same name exists
                         already")
    end
    # Repo is created bare, then clone it in the repository storage location
    repo_path, _sep, repo_name = connect_string.rpartition(File::SEPARATOR)
    bare_path = File.join(repo_path, 'bare', "#{repo_name}.git")
    Rugged::Repository.init_at(bare_path, :bare)
    bare_config = Rugged::Config.new(File.join(bare_path, 'config'))
    bare_config['core.logAllRefUpdates'] = true # enable reflog to keep track of push dates
    bare_config['gc.reflogExpire'] = 'never' # never garbage collect the reflog
    repo = Rugged::Repository.clone_at(bare_path, connect_string)

    # Do an initial commit with the .required_files.json
    required = Assignment.get_required_files
    required_path = File.join(connect_string, '.required.json')
    File.open(required_path, 'w') do |req|
      req.write(required.to_json)
    end
    repo.index.add('.required.json')

    # Add client-side hooks
    if with_hooks && !MarkusConfigurator.markus_config_repository_client_hooks.empty?
      client_hooks_path = MarkusConfigurator.markus_config_repository_client_hooks
      FileUtils.copy_entry client_hooks_path, File.join(connect_string, 'markus-hooks')
      FileUtils.chmod 0755, File.join(connect_string, 'markus-hooks', 'pre-commit')
      repo.index.add_all('markus-hooks')
    end

    GitRepository.do_commit_and_push(repo, 'Markus', I18n.t('repo.commits.initial'))

    # Set up server-side hooks
    if with_hooks
      MarkusConfigurator.markus_config_repository_hooks.each do |hook_symbol, hook_script|
        FileUtils.ln_s(hook_script, File.join(bare_path, 'hooks', hook_symbol.to_s))
      end
    end

    true
  end

  # Static method: Opens an existing Git repository
  # at location 'connect_string'
  def self.open(connect_string)
    GitRepository.new(connect_string)
  end

  # static method that should yield to a git repo and then close it
  def self.access(connect_string)
    begin
      repo = GitRepository.open(connect_string)
      yield repo
    ensure
      repo&.close
    end
  end

  # static method that deletes the git repo
  # rm everything? or only .git?
  def self.delete(repo_path)
    FileUtils.rm_rf(repo_path)
  end

  def self.get_checkout_command(external_repo_url, revision_hash, group_name, repo_folder=nil)
    "git clone \"#{external_repo_url}\" \"#{group_name}\" && "\
    "cd \"#{group_name}\" && "\
    "git reset --hard #{revision_hash} && "\<commit_msg>style: unnecessary begin block<commit_after># Implements AbstractRepository for Git repositories
# It implements the following paradigm:
#   1. Repositories are created by using ???
#   2. Existing repositories are opened by using either ???
class GitRepository < Repository::AbstractRepository

  DUMMY_FILE_NAME = '.gitkeep'.freeze

  # Constructor: Connects to an existing Git
  # repository, using Ruby bindings; Note: A repository has to be
  # created using GitRepository.create(), if it is not yet existent
  def initialize(connect_string)

    # Check if configuration is in order
    if MarkusConfigurator.markus_config_repository_admin?.nil?
      raise ConfigurationError.new("Required config 'IS_REPOSITORY_ADMIN' not set")
    end
    if MarkusConfigurator.markus_config_repository_storage.nil?
      raise ConfigurationError.new("Required config 'REPOSITORY_STORAGE' not set")
    end
    if MarkusConfigurator.markus_config_repository_permission_file.nil?
      raise ConfigurationError.new("Required config 'REPOSITORY_PERMISSION_FILE' not set")
    end
    begin
      super(connect_string) # dummy call to super
    rescue NotImplementedError; end
    @repos_path = connect_string
    @closed = false
    if GitRepository.repository_exists?(@repos_path)
      begin
        @repos = Rugged::Repository.new(@repos_path)
        # make sure working directory is up-to-date
        @repos.fetch('origin')
        begin
          @repos.reset('master', :hard) # TODO this shouldn't be necessary, but something is messing up the repo.
        rescue Rugged::ReferenceError   # It seems the master branch might not be correctly setup at first.
        end
        @repos.reset('origin/master', :hard) # align to whatever is in origin/master
      rescue StandardError # TODO this shouldn't be necessary. It catches the case when a local repo is corrupted,
                           #      we need to prevent the corruption instead/as well
        GitRepository.reclone_repo(connect_string)
      end
    else
      GitRepository.reclone_repo(connect_string)
    end
  end

  def self.reclone_repo(connect_string)
    repo_path, _sep, repo_name = connect_string.rpartition(File::SEPARATOR)
    bare_path = File.join(repo_path, 'bare', "#{repo_name}.git")
    raise "Cannot reclone from non-existant bare repository #{bare_path}" unless Dir.exist?(bare_path)
    if Dir.exist?(connect_string)
      bad_repo_path = "#{connect_string}.bad"
      FileUtils.rm_rf(bad_repo_path)
      FileUtils.mv(connect_string, bad_repo_path)
    end
    @repos = Rugged::Repository.clone_at(bare_path, connect_string)
    m_logger = MarkusLogger.instance
    m_logger.log "Recloned corrupted or missing git repo: #{connect_string}"
  rescue StandardError
    msg = "Failed to clone corrupted or missing git repo: #{connect_string}"
    m_logger = MarkusLogger.instance
    m_logger.log msg
    raise
  end

  def self.do_commit(repo, author, message)
    index = repo.index
    commit_tree = index.write_tree(repo)
    index.write
    commit_author = {email: '<EMAIL>', name: author, time: Time.now}
    commit_options = {
        author: commit_author,
        committer: commit_author,
        message: message,
        tree: commit_tree,
        parents: repo.empty? ? [] : [repo.head.target].compact, # compact if target returns nil (suggested upstream)
        update_ref: 'HEAD'
    }
    Rugged::Commit.create(repo, commit_options)
  end

  def self.do_commit_and_push(repo, author, message)
    GitRepository.do_commit(repo, author, message)
    repo.push('origin', ['refs/heads/master'])
  end

  # Static method: Creates a new Git repository at
  # location 'connect_string'
  def self.create(connect_string, with_hooks: true)
    if GitRepository.repository_exists?(connect_string)
      raise RepositoryCollision.new("There is already a repository at #{connect_string}")
    end
    if File.exists?(connect_string)
      raise IOError.new("Could not create a repository at #{connect_string}: some directory with same name exists
                         already")
    end
    # Repo is created bare, then clone it in the repository storage location
    repo_path, _sep, repo_name = connect_string.rpartition(File::SEPARATOR)
    bare_path = File.join(repo_path, 'bare', "#{repo_name}.git")
    Rugged::Repository.init_at(bare_path, :bare)
    bare_config = Rugged::Config.new(File.join(bare_path, 'config'))
    bare_config['core.logAllRefUpdates'] = true # enable reflog to keep track of push dates
    bare_config['gc.reflogExpire'] = 'never' # never garbage collect the reflog
    repo = Rugged::Repository.clone_at(bare_path, connect_string)

    # Do an initial commit with the .required_files.json
    required = Assignment.get_required_files
    required_path = File.join(connect_string, '.required.json')
    File.open(required_path, 'w') do |req|
      req.write(required.to_json)
    end
    repo.index.add('.required.json')

    # Add client-side hooks
    if with_hooks && !MarkusConfigurator.markus_config_repository_client_hooks.empty?
      client_hooks_path = MarkusConfigurator.markus_config_repository_client_hooks
      FileUtils.copy_entry client_hooks_path, File.join(connect_string, 'markus-hooks')
      FileUtils.chmod 0755, File.join(connect_string, 'markus-hooks', 'pre-commit')
      repo.index.add_all('markus-hooks')
    end

    GitRepository.do_commit_and_push(repo, 'Markus', I18n.t('repo.commits.initial'))

    # Set up server-side hooks
    if with_hooks
      MarkusConfigurator.markus_config_repository_hooks.each do |hook_symbol, hook_script|
        FileUtils.ln_s(hook_script, File.join(bare_path, 'hooks', hook_symbol.to_s))
      end
    end

    true
  end

  # Static method: Opens an existing Git repository
  # at location 'connect_string'
  def self.open(connect_string)
    GitRepository.new(connect_string)
  end

  # static method that should yield to a git repo and then close it
  def self.access(connect_string)
    repo = GitRepository.open(connect_string)
    yield repo
  ensure
    repo&.close
  end

  # static method that deletes the git repo
  # rm everything? or only .git?
  def self.delete(repo_path)
    FileUtils.rm_rf(repo_path)
  end

  def self.get_checkout_command(external_repo_url, revision_hash, group_name, repo_folder=nil)
    "git clone \"#{external_repo_url}\" \"#{group_name}\" && "\
    "cd \"#{group_name}\" && "\
    "git reset --hard #{revision_hash} && "\
{'fraction_non_alphanumeric': 0.09126153369000543, 'fraction_numerical': 0.0012405985888191051, 'mean_word_length': 3.6731884057971014, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1971142', 'n_tokens_mistral': 3919, 'n_tokens_neox': 3858, 'n_words': 1021}