lib/gitolite/config.rb
# frozen_string_literal: true
require 'tempfile'
module Gitolite
class Config
attr_accessor :repos, :groups, :filename
def self.init(filename = 'gitolite.conf')
file = Tempfile.new(filename)
conf = self.new(file.path)
conf.filename = filename # kill suffix added by Tempfile
file.close(unlink_now = true)
conf
end
def initialize(config)
@repos = {}
@groups = {}
@filename = File.basename(config)
process_config(config)
end
# TODO: merge repo unless overwrite = true
def add_repo(repo, overwrite = false)
raise ArgumentError, 'Repo must be of type Gitolite::Config::Repo!' unless repo.instance_of? Gitolite::Config::Repo
@repos[repo.name] = repo
end
def rm_repo(repo)
name = normalize_repo_name(repo)
@repos.delete(name)
end
def has_repo?(repo)
name = normalize_repo_name(repo)
@repos.key?(name)
end
def get_repo(repo)
name = normalize_repo_name(repo)
@repos[name]
end
def add_group(group, overwrite = false)
raise ArgumentError, 'Group must be of type Gitolite::Config::Group!' unless group.instance_of? Gitolite::Config::Group
@groups[group.name] = group
end
def rm_group(group)
name = normalize_group_name(group)
@groups.delete(name)
end
def has_group?(group)
name = normalize_group_name(group)
@groups.key?(name)
end
def get_group(group)
name = normalize_group_name(group)
@groups[name]
end
def to_file(path='.', filename=@filename)
raise ArgumentError, 'Path contains a filename or does not exist' unless File.directory?(path)
new_conf = File.join(path, filename)
File.open(new_conf, 'w') do |f|
f.sync = true
# Output groups
dep_order = build_groups_depgraph
dep_order.each { |group| f.write group.to_s }
gitweb_descs = []
@repos.sort.each do |k, v|
f.write "\n"
f.write v.to_s
gwd = v.gitweb_description
gitweb_descs.push(gwd) unless gwd.nil?
end
f.write "\n"
f.write gitweb_descs.join("\n")
end
new_conf
end
private
# Based on
# https://github.com/sitaramc/gitolite/blob/pu/src/gl-compile-conf#cleanup_conf_line
def cleanup_config_line(line)
# remove comments, even those that happen inline
line.gsub!(/^((".*?"|[^#"])*)#.*/) { |m| m=$1 }
# fix whitespace
line.gsub!('=', ' = ')
line.gsub!(/\s+/, ' ')
line.strip
end
def process_config(config)
context = [] # will store our context for permissions or config declarations
# Read each line of our config
File.open(config, 'r').each do |l|
line = cleanup_config_line(l)
next if line.empty? # lines are empty if we killed a comment
case line
# found a repo definition
when /^repo (.*)/
# Empty our current context
context = []
repos = $1.split
repos.each do |r|
context << r
@repos[r] = Repo.new(r) unless has_repo?(r)
end
# repo permissions
when /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/
perm = $1
refex = $2 || ''
users = $3.split
context.each do |c|
@repos[c].add_permission(perm, refex, users)
end
# repo git config
when /^config (.+) = ?(.*)/
key = $1
value = $2
context.each do |c|
@repos[c].set_git_config(key, value)
end
# repo gitolite option
when /^option (.+) = (.*)/
key = $1
value = $2
raise ParseError, "Missing gitolite option value for repo: #{repo} and key: #{key}" if value.nil?
context.each do |c|
@repos[c].set_gitolite_option(key, value)
end
# group definition
when /^#{Group::PREPEND_CHAR}(\S+) = ?(.*)/
group = $1
users = $2.split
@groups[group] = Group.new(group) unless has_group?(group)
@groups[group].add_users(users)
# gitweb definition
when /^(\S+)(?: "(.*?)")? = "(.*)"$/
repo = $1
owner = $2
description = $3
# Check for missing description
raise ParseError, "Missing Gitweb description for repo: #{repo}" if description.nil?
# Check for groups
raise ParseError, 'Gitweb descriptions cannot be set for groups' if repo =~ /@.+/
if has_repo? repo
r = @repos[repo]
else
r = Repo.new(repo)
add_repo(r)
end
r.owner = owner
r.description = description
when /^include "(.+)"/
# TODO: implement includes
# ignore includes for now
when /^subconf (\S+)$/
# TODO: implement subconfs
# ignore subconfs for now
else
raise ParseError, "'#{line}' cannot be processed"
end
end
end
# Normalizes the various different input objects to Strings
def normalize_name(context, constant = nil)
case context
when constant
context.name
when Symbol
context.to_s
else
context
end
end
def method_missing(meth, *args, &block)
if meth.to_s =~ /normalize_(\w+)_name/
# Could use Object.const_get to figure out the constant here
# but for only two cases, this is more readable
case $1
when 'repo'
normalize_name(args[0], Gitolite::Config::Repo)
when 'group'
normalize_name(args[0], Gitolite::Config::Group)
end
else
super
end
end
# Builds a dependency tree from the groups in order to ensure all groups
# are defined before they are used
def build_groups_depgraph
dp = ::GRATR::Digraph.new
# Add each group to the graph
@groups.each_value do |group|
dp.add_vertex! group
# Select group names from the users
subgroups = group.users.select { |u| u =~ /^#{Group::PREPEND_CHAR}.*$/ }.map { |g| get_group g.gsub(Group::PREPEND_CHAR, '') }
subgroups.each do |subgroup|
dp.add_edge! subgroup, group
end
end
# Figure out if we have a good depedency graph
dep_order = dp.topsort
if dep_order.empty?
raise GroupDependencyError unless @groups.empty?
end
dep_order
end
# Raised when something in a config fails to parse properly
class ParseError < RuntimeError
end
# Raised when group dependencies cannot be suitably resolved for output
class GroupDependencyError < RuntimeError
end
end
end