lib/gitolite/gitolite_admin.rb
# 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