require 'tempfile' require 'fileutils' 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.has_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.has_key?(name) end def get_group(group) name = normalize_group_name(group) @groups[name] end def to_file(path=".", filename=@filename) FileUtils.mkdir_p(path) 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 # On first call with a custom *.conf, the config might not yet exist return unless File.exists?(config) #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