jbox-web/gitolite

View on GitHub
lib/gitolite/gitolite_admin.rb

Summary

Maintainability
A
2 hrs
Test Coverage
C
78%
# frozen_string_literal: true

module Gitolite
  class GitoliteAdmin

    attr_accessor :gl_admin

    CONFIG_FILE = 'gitolite.conf'
    CONF_DIR    = 'conf'
    KEY_DIR     = 'keydir'
    DEBUG       = false
    TIMEOUT     = 10

    # See here form mode details :https://github.com/gitlabhq/grit/blob/master/lib/grit/git.rb#L283
    RAISE_ERROR = true

    # Gitolite gem's default commit message
    DEFAULT_COMMIT_MSG = 'Committed by the gitolite gem'

    class << self

      # Checks to see if the given path is a gitolite-admin repository
      # A valid repository contains a conf folder, keydir folder,
      # and a configuration file within the conf folder
      def is_gitolite_admin_repo?(dir)
        # First check if it is a git repository
        begin
          Grit::Repo.new(dir)
        rescue Grit::NoSuchPathError, Grit::InvalidGitRepositoryError
          return false
        end

        # If we got here it is a valid git repo,
        # now check directory structure
        File.exist?(File.join(dir, 'conf')) &&
          File.exist?(File.join(dir, 'keydir')) &&
          !Dir.glob(File.join(dir, 'conf', '*.conf')).empty?
      end


      # This method will bootstrap a gitolite-admin repo
      # at the given path.  A typical gitolite-admin
      # repo will have the following tree:
      #
      # gitolite-admin
      #   conf
      #     gitolite.conf
      #   keydir
      def bootstrap(path, options = {})
        if self.is_gitolite_admin_repo?(path)
          if options[:overwrite]
            FileUtils.rm_rf(File.join(path, '*'))
          else
            return self.new(path)
          end
        end

        FileUtils.mkdir_p([File.join(path, 'conf'), File.join(path, 'keydir')])

        options[:perm]  ||= 'RW+'
        options[:refex] ||= ''
        options[:user]  ||= 'git'

        c = Config.init
        r = Config::Repo.new(options[:repo] || 'gitolite-admin')
        r.add_permission(options[:perm], options[:refex], options[:user])
        c.add_repo(r)
        config = c.to_file(File.join(path, 'conf'))

        gl_admin = Grit::Repo.init(path)
        gl_admin.git.native(:add, {}, config)
        gl_admin.git.native(:commit, {}, '-a', '-m', options[:message] || 'Config bootstrapped by the gitolite gem')

        self.new(path)
      end

    end


    # Intialize with the path to
    # the gitolite-admin repository
    def initialize(path, options = {})
      @path = path

      @config_file = options[:config_file] || CONFIG_FILE
      @conf_dir    = options[:conf_dir] || CONF_DIR
      @key_dir     = options[:key_dir] || KEY_DIR
      git_env      = options[:env] || {}
      git_raise    = options[:raise] || RAISE_ERROR

      @git_options = { env: git_env, raise: git_raise }

      @config_file_path = File.join(@path, @conf_dir, @config_file)
      @conf_dir_path    = File.join(@path, @conf_dir)
      @key_dir_path     = File.join(@path, @key_dir)

      Grit::Git.git_timeout = options[:timeout] || TIMEOUT
      Grit.debug = options[:debug] || DEBUG
      @gl_admin  = Grit::Repo.new(path)

      reload!
    end


    def git_options
      @git_options.clone
    end


    def config
      @config ||= load_config
    end


    def config=(config)
      @config = config
    end


    def ssh_keys
      @ssh_keys ||= load_keys
    end


    def add_key(key)
      raise 'Key must be of type Gitolite::SSHKey!' unless key.instance_of? Gitolite::SSHKey
      ssh_keys[key.owner] << key
    end


    def rm_key(key)
      raise 'Key must be of type Gitolite::SSHKey!' unless key.instance_of? Gitolite::SSHKey
      ssh_keys[key.owner].delete key
    end


    # This method will destroy all local tracked changes, resetting the local gitolite
    # git repo to HEAD and reloading the entire repository
    # Note that this will also delete all untracked files
    def reset!
      @gl_admin.git.native(:reset, git_options.merge(hard: true), 'HEAD')
      @gl_admin.git.native(:clean, git_options.merge(d: true, q: true, f: true))
      reload!
    end


    # This method will destroy the in-memory data structures and reload everything
    # from the file system
    def reload!
      @ssh_keys = load_keys
      @config = load_config
    end


    # Writes all changed aspects out to the file system
    # will also stage all changes then commit
    def save(commit_message = DEFAULT_COMMIT_MSG, options = {})

      # Process config file (if loaded, i.e. may be modified)
      if @config
        new_conf = @config.to_file(@conf_dir_path)
        @gl_admin.git.native(:add, git_options, new_conf)
      end

      # Process ssh keys (if loaded, i.e. may be modified)
      if @ssh_keys
        files = list_keys.map { |f| File.basename f }
        keys  = @ssh_keys.values.map { |f| f.map(&:filename) }.flatten

        to_remove = (files - keys).map { |f| File.join(@key_dir, f) }
        to_remove.each do |key|
          @gl_admin.git.native(:rm, git_options, key)
        end

        @ssh_keys.each_value do |key|
          # Write only keys from sets that has been modified
          next if key.respond_to?(:dirty?) && !key.dirty?
          key.each do |k|
            new_key = k.to_file(@key_dir_path)
            @gl_admin.git.native(:add, git_options, new_key)
          end
        end
      end

      args = []

      args << '-a'
      args << '-m'
      args << commit_message

      if options.key?(:author) && !options[:author].empty?
        args << "--author='#{options[:author]}'"
      end

      @gl_admin.git.native(:commit, git_options, *args)
    end


    # Push back to origin
    def apply
      @gl_admin.git.native(:push, git_options, 'origin', 'master')
    end


    # Commits all staged changes and pushes back to origin
    def save_and_apply(commit_message = DEFAULT_COMMIT_MSG)
      save(commit_message)
      apply
    end


    # Updates the repo with changes from remote master
    def update(options = {})
      options = { reset: true, rebase: false }.merge(options)

      reset! if options[:reset]

      @gl_admin.git.native(:pull, git_options.merge(rebase: options[:rebase]), 'origin', 'master')

      reload!
    end


    private


    def load_config
      Config.new(@config_file_path)
    end


    def list_keys
      Dir.glob(@key_dir_path + '/**/*.pub')
    end


    # Loads all .pub files in the gitolite-admin
    # keydir directory
    def load_keys
      keys = Hash.new { |k,v| k[v] = DirtyProxy.new([]) }

      list_keys.each do |key|
        new_key = SSHKey.from_file(key)
        owner = new_key.owner

        keys[owner] << new_key
      end

      # Mark key sets as unmodified (for dirty checking)
      keys.values.each(&:clean_up!)

      keys
    end

  end
end