lib/gitolite/gitolite_admin.rb
require 'pathname'
module Gitolite
class GitoliteAdmin
attr_accessor :repo
# Default settings
DEFAULTS = {
# clone/push url settings
git_user: 'git',
hostname: 'localhost',
# Commit settings
author_name: 'gitolite-rugged gem',
author_email: 'gitolite-rugged@localhost',
commit_msg: 'Commited by the gitolite-rugged gem',
# Gitolite-Admin settings
config_dir: "conf",
key_dir: "keydir",
key_subdir: "",
config_file: "gitolite.conf",
lock_file_path: '.lock',
# Repo settings
update_on_init: true,
reset_before_update: true
}
class << self
# Checks 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
repo = Rugged::Repository.new(dir)
return false if repo.empty?
rescue Rugged::RepositoryError, Rugged::OSError
return false
end
# Check if config file, key directory exist
[ File.join(dir, DEFAULTS[:config_dir]), File.join(dir, DEFAULTS[:key_dir]),
File.join(dir, DEFAULTS[:config_dir], DEFAULTS[:config_file])
].each { |f| return false unless File.exists?(f) }
true
end
def admin_url(settings)
['ssh://', settings[:git_user], '@', settings[:host], '/gitolite-admin.git'].join
end
end
# Intialize with the path to
# the gitolite-admin repository
#
# Settings:
# [Connection]
# :git_user: The git user to SSH to (:git_user@localhost:gitolite-admin.git), defaults to 'git'
# :private_key: The key file containing the private SSH key for :git_user
# :public_key: The key file containing the public SSH key for :git_user
# :host: Hostname for clone url. Defaults to 'localhost'
#
# [Gitolite-Admin]
# :config_dir: Config directory within gitolite repository (defaults to 'conf')
# :key_dir: Public key directory within gitolite repository (defaults to 'keydir')
# :config_file: Config file to parse (default: 'gitolite.conf')
# **use only when you use the 'include' directive of gitolite)**
# :key_subdir: Where to store gitolite-rugged known keys, defaults to '' (i.e., directly in keydir)
# :lock_file_path: location of the transaction lockfile, defaults to <gitolite-admin.git>/.lock
#
# The settings hash is forwarded to +GitoliteAdmin.new+ as options.
def initialize(path, settings = {})
@path = path
@settings = DEFAULTS.merge(settings)
# Ensure SSH key settings exist
@settings.fetch(:public_key)
@settings.fetch(:private_key)
# setup credentials
@credentials = Rugged::Credentials::SshKey.new(
username: @settings[:git_user],
publickey: settings[:public_key],
privatekey: settings[:private_key]
)
@config_dir_path = File.join(@path, @settings[:config_dir])
@config_file_path = File.join(@config_dir_path, @settings[:config_file])
@key_dir_path = File.join(@path, relative_key_dir)
@commit_author = { email: @settings[:author_email], name: @settings[:author_name] }
if self.class.is_gitolite_admin_repo?(path)
@repo = Rugged::Repository.new(path, credentials: @credentials )
# Update repository
if @settings[:update_on_init]
update
end
else
@repo = clone
end
reload!
end
#
# Returns the relative directory to the gitolite config file location.
# I.e., settings[config_dir]/settings[config_file]
# Defaults to 'conf/gitolite.conf'
def relative_config_file
File.join(@settings[:config_dir], @settings[:config_file])
end
#
# Returns the relative directory to the public key location.
# I.e., settings[key_dir]/settings[key_subdir]
# Defaults to 'keydir/'
def relative_key_dir
File.join(@settings[:key_dir], @settings[:key_subdir])
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)
unless key.instance_of? Gitolite::SSHKey
raise GitoliteAdminError, "Key must be of type Gitolite::SSHKey!"
end
ssh_keys[key.owner] << key
end
def rm_key(key)
unless key.instance_of? Gitolite::SSHKey
raise GitoliteAdminError, "Key must be of type Gitolite::SSHKey!"
end
ssh_keys[key.owner].delete key
end
# This method will destroy all local tracked changes, resetting the local gitolite
# git repo to HEAD
def reset!
@repo.reset('origin/master', :hard)
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_msg = nil)
# Add all changes to index (staging area)
index = @repo.index
#Process config file (if loaded, i.e. may be modified)
if @config
new_conf = @config.to_file(path=@config_dir_path)
index.add(relative_config_file)
end
#Process ssh keys (if loaded, i.e. may be modified)
if @ssh_keys
files = list_keys.map{|f| relative_key_path(f) }
keys = @ssh_keys.values.map{|f| f.map {|t| t.relative_path}}.flatten
to_remove = (files - keys).each do |key|
SSHKey.remove(key, @key_dir_path)
index.remove File.join(relative_key_dir, 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)
index.add File.join(relative_key_dir, k.relative_path)
end
end
end
# Write index to git and resync fs
commit_tree = index.write_tree @repo
index.write
commit_author = @commit_author.merge(time: Time.now)
Rugged::Commit.create(@repo,
author: commit_author,
committer: commit_author,
message: commit_msg || @settings[:commit_msg],
parents: [repo.head.target],
tree: commit_tree,
update_ref: 'HEAD'
)
end
# Push back to origin
def apply
@repo.push('origin', ['refs/heads/master'], credentials: @credentials)
end
# Commits all staged changes and pushes back to origin
def save_and_apply()
save
apply
end
# Lock the gitolite-admin directory and yield.
# After the block is completed, calls +apply+ only.
# You have to commit your changes within the transaction block
def transaction
get_lock do
yield
# Push all changes
apply
end
end
# Updates the repo with changes from remote master
# Warning: This resets the repo before pulling in the changes.
def update()
# Reset --hard repo before update
if @settings[:reset_before_update]
reset!
end
# Fetch changes from origin
@repo.fetch('origin', credentials: @credentials )
# Currently, only merging from origin/master into master is supported.
master = @repo.references["refs/heads/master"].target
origin_master = @repo.references["refs/remotes/origin/master"].target
# Create the merged index in memory
merge_index = repo.merge_commits(master, origin_master)
# Complete the merge by comitting it
merge_commit = Rugged::Commit.create(@repo,
parents: [ master, origin_master ],
tree: merge_index.write_tree(@repo),
message: '[gitolite-rugged] Merged `origin/master` into `master`',
author: @commit_author,
committer: @commit_author,
update_ref: 'refs/heads/master'
)
reload!
end
private
# Clone the gitolite-admin repo
# to the given path.
#
# The repo is cloned from the url
# +(:git_user)@(:hostname)/gitolite-admin.git+
#
# The hostname may use an optional :port to allow for custom SSH ports.
# E.g., +git@localhost:2222/gitolite-admin.git+
#
def clone
Rugged::Repository.clone_at(GitoliteAdmin.admin_url(@settings), File.expand_path(@path), credentials: @credentials )
end
def load_config
Config.new(@config_file_path)
end
def list_keys
Dir.glob(@key_dir_path + '/**/*.pub')
end
# Returns the relative key path
# <owner>/<location>/<owner> given an absolute path
# below the keydir.
def relative_key_path(key_path)
Pathname.new(key_path).relative_path_from(Pathname.new(@key_dir_path)).to_s
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{|set| set.clean_up!}
keys
end
def lock_file_path
File.expand_path(@settings[:lock_file_path], @path)
end
# Aquire LOCK_EX on the gitolite-admin.git directory .
# Use +GitoliteAdmin.transaction+ to modify with flock.
def get_lock
File.open(lock_file_path, File::RDWR|File::CREAT, 0644) do |file|
file.sync = true
file.flock(File::LOCK_EX)
yield
file.flock(File::LOCK_UN)
end
end
end
end