Añade plugin Redmine Git Hosting 4.0.2

This commit is contained in:
Manuel Cillero 2020-12-05 13:57:05 +01:00
parent 472cb1ea76
commit bdd66d941f
494 changed files with 36768 additions and 0 deletions

View file

@ -0,0 +1,70 @@
---
# Gitolite SSH Config
gitolite_user: 'git'
gitolite_server_host: '127.0.0.1'
gitolite_server_port: '22'
gitolite_ssh_private_key: <%= Rails.root.join('plugins', 'redmine_git_hosting', 'ssh_keys', 'redmine_gitolite_admin_id_rsa') %>
gitolite_ssh_public_key: <%= Rails.root.join('plugins', 'redmine_git_hosting', 'ssh_keys', 'redmine_gitolite_admin_id_rsa.pub') %>
# Gitolite Storage Config
gitolite_global_storage_dir: 'repositories/'
gitolite_redmine_storage_dir: ''
gitolite_recycle_bin_dir: 'recycle_bin/'
# Gitolite Config File
gitolite_config_file: 'gitolite.conf'
gitolite_identifier_prefix: 'redmine_'
gitolite_identifier_strip_user_id: 'false'
# Gitolite Global Config
gitolite_temp_dir: <%= Rails.root.join('tmp', 'redmine_git_hosting') %>
gitolite_recycle_bin_expiration_time: '24.0'
gitolite_log_level: 'info'
git_config_username: 'Redmine Git Hosting'
git_config_email: 'redmine@example.net'
# Gitolite Hooks Config
gitolite_overwrite_existing_hooks: 'true'
gitolite_hooks_are_asynchronous: 'false'
gitolite_hooks_debug: 'false'
gitolite_hooks_url: 'http://localhost:3000'
# Gitolite Cache Config
gitolite_cache_max_time: '86400'
gitolite_cache_max_size: '16'
gitolite_cache_max_elements: '2000'
gitolite_cache_adapter: 'database'
# Gitolite Access Config
ssh_server_domain: 'localhost'
http_server_domain: 'localhost'
https_server_domain: 'localhost'
http_server_subdir: ''
show_repositories_url: 'true'
gitolite_daemon_by_default: 'false'
gitolite_http_by_default: '1'
# Redmine Config
redmine_has_rw_access_on_all_repos: 'true'
all_projects_use_git: 'false'
init_repositories_on_create: 'false'
delete_git_repositories: 'true'
# This params work together!
# When hierarchical_organisation = true unique_repo_identifier MUST be false
# When hierarchical_organisation = false unique_repo_identifier MUST be true
hierarchical_organisation: 'true'
unique_repo_identifier: 'false'
# Download Revision Config
download_revision_enabled: 'true'
# Git Mailing List Config
gitolite_notify_by_default: 'false'
gitolite_notify_global_prefix: '[REDMINE]'
gitolite_notify_global_sender_address: 'redmine@example.net'
gitolite_notify_global_include: []
gitolite_notify_global_exclude: []
# Sidekiq Config
gitolite_use_sidekiq: 'false'

View file

@ -0,0 +1,2 @@
$LOAD_PATH.push File.expand_path('lib', __dir__)
require 'hrack'

View file

@ -0,0 +1,3 @@
module Hrack
require 'hrack/bundle'
end

View file

@ -0,0 +1,16 @@
require 'rack/builder'
require 'rack/parser'
require 'hrack/server'
module Hrack
module Bundle
module_function
def new(config)
Rack::Builder.new do
use Rack::Parser
run Hrack::Server.new(config)
end
end
end
end

View file

@ -0,0 +1,142 @@
require 'digest/sha1'
module Hrack
class Server
attr_reader :params
PLAIN_TYPE = { 'Content-Type' => 'text/plain' }
def initialize(config = {})
end
def call(env)
dup._call(env)
end
def _call(env)
@env = env
@req = Rack::Request.new(env)
@params = @req.params.deep_symbolize_keys
command, @project = match_routing
return render_404('Command Not Found') if !command
return render_404('Project Not Found') if !@project
self.method(command).call()
end
def post_receive_redmine
@repository = find_repository
return render_404('Repository Not Found') if @repository.nil?
return render_403('The hook key provided is not valid. Please let your server admin know about it') if !valid_encoded_time?(params[:clear_time], params[:encoded_time], @repository.gitolite_hook_key)
@res = Rack::Response.new
@res.status = 200
@res['Content-Type'] = 'text/plain;'
@res.finish do
@res.write Repositories::ExecuteHooks.call(@repository, :fetch_changesets)
@res.write Repositories::ExecuteHooks.call(@repository, :update_mirrors, payloads)
@res.write Repositories::ExecuteHooks.call(@repository, :call_webservices, payloads)
end
end
def post_receive_github
Projects::ExecuteHooks.call(@project, :github, params)
render_200('OK!')
end
private
def payloads
@payloads ||= Repositories::BuildPayload.call(@repository, params[:refs])
end
def match_routing
command = find_command
project = find_project
return command, project
end
def find_command
return nil if !path_parameters.has_key?(:type)
case path_parameters[:type]
when 'redmine'
:post_receive_redmine
when 'github'
:post_receive_github
else
nil
end
end
def find_project
if path_parameters.has_key?(:projectid)
Project.find_by_identifier(path_parameters[:projectid])
else
nil
end
end
# Locate that actual repository that is in use here.
# Notice that an empty "repositoryid" is assumed to refer to the default repo for a project
def find_repository
if params[:repositoryid] && !params[:repositoryid].blank?
@project.repositories.find_by_identifier(params[:repositoryid])
else
# return default or first repo with blank identifier
@project.repository || @project.repo_blank_ident
end
end
def render_200(message)
[200, PLAIN_TYPE, [message]]
end
def render_404(message)
[404, PLAIN_TYPE, [message]]
end
def render_403(message)
[403, PLAIN_TYPE, [message]]
end
def path_parameters
@env['action_dispatch.request.path_parameters']
end
def valid_encoded_time?(clear_time, encoded_time, key)
cur_time = Time.new.utc.to_i
test_time = clear_time.to_i
not_to_late?(cur_time, test_time) && encode_key(clear_time, key) == encoded_time.to_s
end
def not_to_late?(cur_time, test_time)
cur_time - test_time < 5 * 60
end
def encode_key(time, key)
Digest::SHA1.hexdigest(time.to_s + key.to_s).to_s
end
end
end

View file

@ -0,0 +1,3 @@
module Hrack
VERSION = '1.0.0'
end

View file

@ -0,0 +1,73 @@
RedmineGitHosting::GitoliteHooks.register_hooks do
# Set source dir
source_dir Rails.root.join('plugins', 'redmine_git_hosting', 'contrib', 'hooks').to_s
# Declare GitoliteHooks to install
# Install executable
gitolite_hook do
name 'redmine_gitolite.rb'
source 'post-receive/redmine_gitolite.rb'
destination 'post-receive'
executable true
end
gitolite_hook do
name 'mail_notifications.py'
source 'post-receive/mail_notifications.py'
destination 'post-receive.d/mail_notifications'
executable true
end
# Install libs
gitolite_hook do
name 'git_hosting_config.rb'
source 'post-receive/lib/git_hosting_config.rb'
destination 'lib/git_hosting/config.rb'
executable false
end
gitolite_hook do
name 'git_hosting_custom_hook.rb'
source 'post-receive/lib/git_hosting_custom_hook.rb'
destination 'lib/git_hosting/custom_hook.rb'
executable false
end
gitolite_hook do
name 'git_hosting_http_helper.rb'
source 'post-receive/lib/git_hosting_http_helper.rb'
destination 'lib/git_hosting/http_helper.rb'
executable false
end
gitolite_hook do
name 'git_hosting_hook_logger.rb'
source 'post-receive/lib/git_hosting_hook_logger.rb'
destination 'lib/git_hosting/hook_logger.rb'
executable false
end
gitolite_hook do
name 'git_hosting_post_receive.rb'
source 'post-receive/lib/git_hosting_post_receive.rb'
destination 'lib/git_hosting/post_receive.rb'
executable false
end
gitolite_hook do
name 'git_multimail.py'
source 'post-receive/lib/git_multimail.py'
destination 'post-receive.d/git_multimail.py'
executable false
end
end
# Gitolite hooks can be found in Redmine root dir or in plugin root dir
[
Rails.root.join('redmine_git_hosting_hooks.rb').to_s,
Rails.root.join('plugins', 'redmine_git_hosting', 'custom_hooks.rb').to_s
].each do |file|
require_dependency file if File.exist?(file)
end

View file

@ -0,0 +1,580 @@
require 'redmine/scm/adapters/abstract_adapter'
# XitoliteAdapter inherits from GitAdapter but some classes which are define directly in GitAdapter are not inherited
# (GitBranch, ScmCommandAborted and maybe others) so it raises NameError exception.
# To fix this I had to reimplement (copy/past) the whole GitAdapter class in XitoliteAdapter...
# I wanted to avoid it to avoid code duplication but it seems to be the only way...
module Redmine
module Scm
module Adapters
class XitoliteAdapter < AbstractAdapter
# Git executable name
XITOLITE_BIN = Redmine::Configuration['scm_git_command'] || "git"
class GitBranch < Branch
attr_accessor :is_default
end
class << self
def client_command
@@bin ||= XITOLITE_BIN
end
def sq_bin
@@sq_bin ||= shell_quote_command
end
def client_version
@@client_version ||= (scm_command_version || [])
end
def client_available
!client_version.empty?
end
def scm_command_version
scm_version = scm_version_from_command_line.dup
if scm_version.respond_to?(:force_encoding)
scm_version.force_encoding('ASCII-8BIT')
end
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i)
end
end
# Change from the original method
def scm_version_from_command_line
RedmineGitHosting::Commands.git_version
end
end
def initialize(url, root_url = nil, login = nil, password = nil, path_encoding = nil)
super
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
end
def path_encoding
@path_encoding
end
def info
begin
Info.new(root_url: url, lastrev: lastrev('', nil))
rescue
nil
end
end
def branches
return @branches if !@branches.nil?
@branches = []
cmd_args = %w|branch --no-color --verbose --no-abbrev|
git_cmd(cmd_args) do |io|
io.each_line do |line|
branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
bran = GitBranch.new(branch_rev[2].to_s.force_encoding(Encoding::UTF_8))
bran.revision = branch_rev[3]
bran.scmid = branch_rev[3]
bran.is_default = (branch_rev[1] == '*')
@branches << bran
end
end
@branches.sort!
rescue ScmCommandAborted => e
logger.error(e.message)
[]
end
def tags
return @tags if !@tags.nil?
@tags = []
cmd_args = %w|tag|
git_cmd(cmd_args) do |io|
@tags = io.readlines.sort!.map { |t| t.strip.force_encoding(Encoding::UTF_8) }
end
@tags
rescue ScmCommandAborted => e
logger.error(e.message)
[]
end
def default_branch
bras = self.branches
return nil if bras.nil?
default_bras = bras.select { |x| x.is_default == true }
return default_bras.first.to_s if !default_bras.empty?
master_bras = bras.select { |x| x.to_s == 'master' }
master_bras.empty? ? bras.first.to_s : 'master'
end
def entry(path = nil, identifier = nil)
parts = path.to_s.split(%r{[\/\\]}).select { |n| !n.blank? }
search_path = parts[0..-2].join('/')
search_name = parts[-1]
if search_path.blank? && search_name.blank?
# Root entry
Entry.new(path: '', kind: 'dir')
else
# Search for the entry in the parent directory
es = entries(search_path, identifier, report_last_commit: false)
es ? es.detect { |e| e.name == search_name } : nil
end
end
def entries(path = nil, identifier = nil, options = {})
path ||= ''
p = scm_iconv(@path_encoding, 'UTF-8', path)
entries = Entries.new
cmd_args = %w|ls-tree -l|
cmd_args << "HEAD:#{p}" if identifier.nil?
cmd_args << "#{identifier}:#{p}" if identifier
git_cmd(cmd_args) do |io|
io.each_line do |line|
e = line.chomp.to_s
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
type = $1
sha = $2
size = $3
name = $4
if name.respond_to?(:force_encoding)
name.force_encoding(@path_encoding)
end
full_path = p.empty? ? name : "#{p}/#{name}"
n = scm_iconv('UTF-8', @path_encoding, name)
full_p = scm_iconv('UTF-8', @path_encoding, full_path)
entries << Entry.new({:name => n,
:path => full_p,
:kind => (type == "tree") ? 'dir' : 'file',
:size => (type == "tree") ? nil : size,
:lastrev => options[:report_last_commit] ?
lastrev(full_path, identifier) : Revision.new
}) unless entries.detect { |entry| entry.name == name }
end
end
end
entries.sort_by_name
rescue ScmCommandAborted => e
logger.error(e.message)
[]
end
def lastrev(path, rev)
return nil if path.nil?
cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
cmd_args << rev if rev
cmd_args << "--" << path unless path.empty?
lines = []
git_cmd(cmd_args) { |io| lines = io.readlines }
begin
id = lines[0].split[1]
author = lines[1].match('Author:\s+(.*)$')[1]
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
Revision.new({
:identifier => id,
:scmid => id,
:author => author,
:time => time,
:message => nil,
:paths => nil
})
rescue NoMethodError => e
logger.error("The revision '#{path}' has a wrong format")
return nil
end
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
def revisions(path, identifier_from, identifier_to, options = {})
revs = Revisions.new
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
cmd_args << "--reverse" if options[:reverse]
cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
revisions = []
if identifier_from || identifier_to
revisions << ''
revisions[0] << "#{identifier_from}.." if identifier_from
revisions[0] << "#{identifier_to}" if identifier_to
else
revisions += options[:includes] unless options[:includes].blank?
revisions += options[:excludes].map { |r| "^#{r}" } unless options[:excludes].blank?
end
git_cmd(cmd_args, { write_stdin: true }) do |io|
io.binmode
io.puts(revisions.join("\n"))
io.close_write
files = []
changeset = {}
# 0: not parsing desc or files
# 1: parsing desc
# 2: parsing files
parsing_descr = 0
io.each_line do |line|
if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
key = 'commit'
value = $1
parents_str = $2
if parsing_descr == 1 || parsing_descr == 2
parsing_descr = 0
revision = Revision.new({
:identifier => changeset[:commit],
:scmid => changeset[:commit],
:author => changeset[:author],
:time => Time.parse(changeset[:date]),
:message => changeset[:description],
:paths => files,
:parents => changeset[:parents]
})
if block_given?
yield revision
else
revs << revision
end
changeset = {}
files = []
end
changeset[:commit] = $1
unless parents_str.nil? or parents_str == ''
changeset[:parents] = parents_str.strip.split(' ')
end
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
key = $1
value = $2
if key == 'Author'
changeset[:author] = value
elsif key == 'CommitDate'
changeset[:date] = value
end
elsif (parsing_descr == 0) && line.chomp.to_s == ''
parsing_descr = 1
changeset[:description] = ''
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $2
p = scm_iconv('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $3
p = scm_iconv('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1) && line.chomp.to_s == ''
parsing_descr = 2
elsif (parsing_descr == 1)
changeset[:description] << line[4..-1]
end
end
if changeset[:commit]
revision = Revision.new({
:identifier => changeset[:commit],
:scmid => changeset[:commit],
:author => changeset[:author],
:time => Time.parse(changeset[:date]),
:message => changeset[:description],
:paths => files,
:parents => changeset[:parents]
})
if block_given?
yield revision
else
revs << revision
end
end
end
revs
rescue ScmCommandAborted => e
err_msg = "git log error: #{e.message}"
logger.error(err_msg)
if block_given?
raise CommandFailed, err_msg
else
revs
end
end
# Override the original method to accept options hash
# which may contain *bypass_cache* flag and pass the options hash to *git_cmd*.
#
def diff(path, identifier_from, identifier_to = nil, opts = {})
path ||= ''
cmd_args = []
if identifier_to
cmd_args << 'diff' << '--no-color' << identifier_to << identifier_from
else
cmd_args << 'show' << '--no-color' << identifier_from
end
cmd_args << '--' << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
diff = []
git_cmd(cmd_args, opts) do |io|
io.each_line do |line|
diff << line
end
end
diff
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
def annotate(path, identifier = nil)
identifier = 'HEAD' if identifier.blank?
cmd_args = %w|blame --encoding=UTF-8|
cmd_args << '-p' << identifier << '--' << scm_iconv(@path_encoding, 'UTF-8', path)
blame = Annotate.new
content = nil
git_cmd(cmd_args) { |io| io.binmode; content = io.read }
# git annotates binary files
return nil if binary_data?(content)
identifier = ''
# git shows commit author on the first occurrence only
authors_by_commit = {}
content.split("\n").each do |line|
if line =~ /^([0-9a-f]{39,40})\s.*/
identifier = $1
elsif line =~ /^author (.+)/
authors_by_commit[identifier] = $1.strip
elsif line =~ /^\t(.*)/
blame.add_line($1, Revision.new(
:identifier => identifier,
:revision => identifier,
:scmid => identifier,
:author => authors_by_commit[identifier]
))
identifier = ''
author = ''
end
end
blame
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
def cat(path, identifier = nil)
identifier = 'HEAD' if identifier.nil?
cmd_args = %w|show --no-color|
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
cat = nil
git_cmd(cmd_args) do |io|
io.binmode
cat = io.read
end
cat
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
# Added to be compatible with EmailDiff plugin
#
def changed_files(path = nil, rev = 'HEAD')
path ||= ''
cmd_args = []
cmd_args << 'log' << '--no-color' << '--pretty=format:%cd' << '--name-status' << '-1' << rev
cmd_args << '--' << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
changed_files = []
git_cmd(cmd_args) do |io|
io.each_line do |line|
changed_files << line
end
end
changed_files
end
# Added for GitDownloadRevision
#
def rev_list(revision, args)
cmd_args = ['rev-list', *args, revision]
git_cmd(cmd_args) do |io|
@revisions_list = io.readlines.map { |t| t.strip }
end
@revisions_list
rescue ScmCommandAborted => e
logger.error(e.message)
[]
end
# Added for GitDownloadRevision / GithubPayload
#
def rev_parse(revision)
cmd_args = ['rev-parse', '--quiet', '--verify', revision]
git_cmd(cmd_args) do |io|
@parsed_revision = io.readlines.map { |t| t.strip }.first
end
@parsed_revision
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
# Added for GitDownloadRevision
#
def archive(revision, format)
cmd_args = ['archive']
case format
when 'tar' then
cmd_args << '--format=tar'
when 'tar.gz' then
cmd_args << '--format=tar.gz'
cmd_args << '-7'
when 'zip' then
cmd_args << '--format=zip'
cmd_args << '-7'
else
cmd_args << '--format=tar'
end
cmd_args << revision
git_cmd(cmd_args, bypass_cache: true) do |io|
io.binmode
@content = io.read
end
@content
rescue ScmCommandAborted => e
logger.error(e.message)
nil
end
# Added for MirrorPush
#
def mirror_push(mirror_url, branch = nil, args = [])
cmd_args = git_mirror_cmd.concat(['push', *args, mirror_url, branch]).compact
cmd = cmd_args.shift
RedmineGitHosting::Utils::Exec.capture(cmd, cmd_args, { merge_output: true })
end
class Revision < Redmine::Scm::Adapters::Revision
# Returns the readable identifier
def format_identifier
identifier[0, 8]
end
end
private
def logger
RedmineGitHosting.logger
end
def git_cmd(args, options = {}, &block)
# Get options
bypass_cache = options.delete(:bypass_cache) { false }
# Build git command line
cmd_str = prepare_command(args)
# Insert cache between shell execution and caller
if !git_cache_id.nil? && git_cache_enabled? && !bypass_cache
RedmineGitHosting::ShellRedirector.execute(cmd_str, git_cache_id, options, &block)
else
Redmine::Scm::Adapters::AbstractAdapter.shellout(cmd_str, options, &block)
end
end
def prepare_command(args)
# Get our basics args
full_args = base_args
# Concat with Redmine args
full_args += args
# Quote args
full_args.map { |e| shell_quote(e.to_s) }.join(' ')
end
# Compute string from repo_path that should be same as: repo.git_cache_id
# If only we had access to the repo (we don't).
# We perform caching here to speed this up, since this function gets called
# many times during the course of a repository lookup.
def git_cache_id
logger.debug("Lookup for git_cache_id with repository path '#{repo_path}' ... ")
@git_cache_id ||= Repository::Xitolite.repo_path_to_git_cache_id(repo_path)
logger.warn("Unable to find git_cache_id for '#{repo_path}', bypass cache... ") if @git_cache_id.nil?
logger.debug("git_cache_id found : #{@git_cache_id}") if !@git_cache_id.nil?
@git_cache_id
end
def base_args
RedmineGitHosting::Commands.sudo_git_args_for_repo(repo_path).concat(git_args)
end
def git_mirror_cmd
RedmineGitHosting::Commands.sudo_git_args_for_repo(repo_path, git_push_args)
end
def git_push_args
['env', "GIT_SSH=#{RedmineGitHosting::Config.gitolite_mirroring_script}"]
end
def repo_path
root_url || url
end
def git_args
self.class.client_version_above?([1, 7, 2]) ? ['-c', 'core.quotepath=false', '-c', 'log.decorate=no'] : []
end
def git_cache_enabled?
RedmineGitHosting::Config.gitolite_cache_max_time != 0
end
def binary_data?(content)
if Gem::Version.new(Redmine::VERSION.to_s) >= Gem::Version.new('3.4')
ScmData.binary?(content)
else
content.is_binary_data?
end
end
end
end
end
end

View file

@ -0,0 +1,63 @@
# Redmine Permissions
require 'redmine_permissions'
# Redmine Menus
require 'redmine_menus'
# Redmine Hooks
require 'redmine_git_hosting/hooks'
# Redmine SCM
Redmine::Scm::Base.add 'Xitolite'
module RedmineGitHosting
extend self
# Load RedminePluginLoader
require 'redmine_git_hosting/redmine_plugin_loader'
extend RedminePluginLoader
set_plugin_name 'redmine_git_hosting'
set_autoloaded_paths 'forms',
'presenters',
'reports',
'services',
'use_cases',
['controllers', 'concerns'],
['models', 'concerns']
def logger
@logger ||= RedmineGitHosting::Logger.init_logs!('RedmineGitHosting', logfile, loglevel)
end
def logfile
Rails.root.join('log', 'git_hosting.log')
end
def loglevel
case RedmineGitHosting::Config.gitolite_log_level
when 'debug' then
Logger::DEBUG
when 'info' then
Logger::INFO
when 'warn' then
Logger::WARN
when 'error' then
Logger::ERROR
else
Logger::INFO
end
end
end
# Set up autoload of patches
Rails.configuration.to_prepare do
# Redmine Git Hosting Libs and Patches
RedmineGitHosting.load_plugin!
# Redmine SCM adapter
require_dependency 'redmine/scm/adapters/xitolite_adapter'
require 'hrack/init'
end

View file

@ -0,0 +1,12 @@
module RedmineGitHosting
class Auth
def find(login, password)
user = User.find_by_login(login)
# Return if user not found
return nil if user.nil?
# Return user if password matches
user if user.check_password?(password)
end
end
end

View file

@ -0,0 +1,62 @@
module RedmineGitHosting
module Cache
extend self
# Used in ShellRedirector but define here to keep a clean interface.
#
def max_cache_size
RedmineGitHosting::Config.gitolite_cache_max_size
end
def set_cache(repo_id, out_value, primary_key, secondary_key = nil)
return if out_value.strip.empty?
command = compose_key(primary_key, secondary_key)
adapter.apply_cache_limit if adapter.set_cache(repo_id, command, out_value)
end
def get_cache(repo_id, primary_key, secondary_key = nil)
command = compose_key(primary_key, secondary_key)
cached = adapter.get_cache(repo_id, command)
# Return result as a string stream
cached.nil? ? nil : StringIO.new(cached)
end
def flush_cache!
adapter.flush_cache!
end
# After resetting cache timing parameters -- delete entries that no-longer match
def clear_obsolete_cache_entries
adapter.clear_obsolete_cache_entries
end
# Clear the cache entries for given repository / git_cache_id
def clear_cache_for_repository(repo_id)
adapter.clear_cache_for_repository(repo_id)
end
def adapter
case RedmineGitHosting::Config.gitolite_cache_adapter
when 'database'
Database
when 'memcached'
Memcached
when 'redis'
Redis
else
Database
end
end
private
def compose_key(key1, key2)
if key2&.present?
key1 + "\n" + key2
else
key1
end
end
end
end

View file

@ -0,0 +1,77 @@
module RedmineGitHosting
module Cache
class AbstractCache
class << self
def max_cache_size
@max_cache_size ||= RedmineGitHosting::Config.gitolite_cache_max_size
end
def max_cache_time
@max_cache_time ||= RedmineGitHosting::Config.gitolite_cache_max_time
end
def max_cache_elements
@max_cache_elements ||= RedmineGitHosting::Config.gitolite_cache_max_elements
end
def set_cache(repo_id, command, output)
raise NotImplementedError
end
def get_cache(repo_id, command)
raise NotImplementedError
end
def flush_cache!
raise NotImplementedError
end
def clear_obsolete_cache_entries
raise NotImplementedError
end
def clear_cache_for_repository(repo_id)
raise NotImplementedError
end
def apply_cache_limit
raise NotImplementedError
end
private
def logger
RedmineGitHosting.logger
end
def time_limit
return if max_cache_time < 0 # No expiration needed
current_time = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
limit = current_time - max_cache_time
limit
end
def valid_cache_entry?(cached_entry_date)
return true if max_cache_time < 0 # No expiration needed
current_time = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
expired = current_time.to_i - cached_entry_date.to_i > max_cache_time
!expired
end
end
end
end
end

View file

@ -0,0 +1,55 @@
module RedmineGitHosting
module Cache
class Database < AbstractCache
class << self
def set_cache(repo_id, command, output)
logger.debug("DB Adapter : inserting cache entry for repository '#{repo_id}'")
begin
GitCache.create(command: command, command_output: output, repo_identifier: repo_id)
true
rescue => e
logger.error("DB Adapter : could not insert in cache, this is the error : '#{e.message}'")
false
end
end
def get_cache(repo_id, command)
cached = GitCache.find_by_repo_identifier_and_command(repo_id, command)
if cached
if valid_cache_entry?(cached.created_at)
# Update updated_at flag
cached.touch unless cached.command_output.nil?
out = cached.command_output
else
cached.destroy
out = nil
end
else
out = nil
end
out
end
def flush_cache!
ActiveRecord::Base.connection.execute('TRUNCATE git_caches')
end
def clear_obsolete_cache_entries
return if time_limit.nil?
deleted = GitCache.where('created_at < ?', time_limit).delete_all
logger.info("DB Adapter : removed '#{deleted}' expired cache entries among all repositories")
end
def clear_cache_for_repository(repo_id)
deleted = GitCache.where(repo_identifier: repo_id).delete_all
logger.info("DB Adapter : removed '#{deleted}' expired cache entries for repository '#{repo_id}'")
end
def apply_cache_limit
GitCache.order(:created_at).first.destroy if max_cache_elements >= 0 && GitCache.count > max_cache_elements
end
end
end
end
end

View file

@ -0,0 +1,99 @@
require 'dalli'
require 'digest/sha1'
module RedmineGitHosting
module Cache
class Memcached < AbstractCache
class << self
def set_cache(repo_id, command, output)
logger.debug("Memcached Adapter : inserting cache entry for repository '#{repo_id}'")
# Create a SHA256 of the Git command as key id
hashed_command = hash_key(command)
begin
create_or_update_repo_references(repo_id, hashed_command)
client.set(hashed_command, output)
true
rescue => e
logger.error("Memcached Adapter : could not insert in cache, this is the error : '#{e.message}'")
false
end
end
def get_cache(repo_id, command)
client.get(hash_key(command))
end
def flush_cache!
client.flush
end
# Return true, this is done automatically by Memcached with the
# *max_cache_time* params (see below)
#
def clear_obsolete_cache_entries
true
end
def clear_cache_for_repository(repo_id)
# Create a SHA256 of the repo_id as key id
hashed_repo_id = hash_key(repo_id)
# Find repository references in Memcached
repo_references = client.get(hashed_repo_id)
return true if repo_references.nil?
# Delete reference keys
repo_references = repo_references.split(',').select { |r| !r.empty? }
repo_references.map { |key| client.delete(key) }
logger.info("Memcached Adapter : removed '#{repo_references.size}' expired cache entries for repository '#{repo_id}'")
# Reset references count
client.set(hashed_repo_id, '', max_cache_time, raw: true)
end
# Return true. If cache is full, Memcached drop the oldest objects to add new ones.
#
def apply_cache_limit
true
end
private
def create_or_update_repo_references(repo_id, reference)
# Create a SHA256 of the repo_id as key id
hashed_repo_id = hash_key(repo_id)
# Find it in Memcached
repo_references = client.get(hashed_repo_id)
if repo_references.nil?
client.set(hashed_repo_id, reference, max_cache_time, raw: true)
else
client.append(hashed_repo_id, ',' + reference)
end
end
def hash_key(key)
Digest::SHA256.hexdigest(key)
end
def client
@client ||= Dalli::Client.new('localhost:11211', memcached_options)
end
def memcached_options
{ namespace: 'redmine_git_hosting', compress: true, expires_in: max_cache_time, value_max_bytes: max_cache_size }
end
end
end
end
end

View file

@ -0,0 +1,117 @@
require 'redis'
require 'digest/sha1'
module RedmineGitHosting
module Cache
class Redis < AbstractCache
class << self
def set_cache(repo_id, command, output)
logger.debug("Redis Adapter : inserting cache entry for repository '#{repo_id}'")
# Create a SHA256 of the Git command as key id
hashed_command = hash_key(repo_id, command)
# If *max_cache_time* is set to -1 (until next commit) then
# set the cache time to 1 day (we don't know when will be the next commit)
cache_time = (max_cache_time < 0) ? 86_400 : max_cache_time
begin
client.set(hashed_command, output, ex: cache_time)
true
rescue => e
logger.error("Redis Adapter : could not insert in cache, this is the error : '#{e.message}'")
false
end
end
def get_cache(repo_id, command)
logger.debug("Redis Adapter : getting cache entry for repository '#{repo_id}'")
client.get(hash_key(repo_id, command))
end
def flush_cache!
deleted = 0
client.scan_each(match: all_entries) do |key|
client.del(key)
deleted += 1
end
logger.info("Redis Adapter : removed '#{deleted}' expired cache entries among all repositories")
end
# Return true, this is done automatically by Redis with the
# *max_cache_time* params (see above)
#
def clear_obsolete_cache_entries
true
end
def clear_cache_for_repository(repo_id)
deleted = 0
client.scan_each(match: all_entries_for_repo(repo_id)) do |key|
client.del(key)
deleted += 1
end
logger.info("Redis Adapter : removed '#{deleted}' expired cache entries for repository '#{repo_id}'")
end
# Return true.
#
def apply_cache_limit
true
end
private
def redis_namespace
'git_hosting_cache'
end
def all_entries
"#{redis_namespace}:*"
end
def all_entries_for_repo(repo_id)
"#{redis_namespace}:#{digest(repo_id)}:*"
end
# Prefix each key with *git_hosting_cache:* to store them in a subdirectory.
# When flushing cache, get all keys with this prefix and delete them.
# Make SHA256 of the Git command as identifier
#
def hash_key(repo_id, command)
"#{redis_namespace}:#{digest(repo_id)}:#{digest(command)}"
end
def digest(string)
Digest::SHA256.hexdigest(string)[0..16]
end
def client
@client ||= ::Redis.new(redis_options)
end
# Specify the Redis DB.
# However, I don't know exactly how it's used by Redis...
#
def redis_options
{ db: redis_namespace, driver: :hiredis }
end
end
end
end
end

View file

@ -0,0 +1,9 @@
module RedmineGitHosting
module Commands
extend Commands::Base
extend Commands::Git
extend Commands::Gitolite
extend Commands::Ssh
extend Commands::Sudo
end
end

View file

@ -0,0 +1,31 @@
module RedmineGitHosting
module Commands
module Base
extend self
# Wrapper to Open3.capture.
#
def capture(args = [], opts = {})
cmd = args.shift
RedmineGitHosting::Utils::Exec.capture(cmd, args, opts)
end
# Wrapper to Open3.capture.
#
def execute(args = [], opts = {})
cmd = args.shift
RedmineGitHosting::Utils::Exec.execute(cmd, args, opts)
end
private
def logger
RedmineGitHosting.logger
end
end
end
end

View file

@ -0,0 +1,135 @@
module RedmineGitHosting
module Commands
module Git
extend self
############################
# #
# Sudo+Git Shell Wrapper #
# #
############################
# Send Git command with Sudo
#
def sudo_git(*params)
if RedmineGitHosting::Config.gitolite_use_sudo?
cmd = sudo_git_cmd.concat(params)
else
cmd = ['git'].concat(params)
end
capture(cmd)
end
def sudo_git_cmd(args = [])
sudo.concat(git(args))
end
def sudo_git_args_for_repo(repo_path, args = [])
sudo.concat(git(args)).concat(git_args_for_repo(repo_path))
end
def sudo_unset_git_global_param(key)
logger.info("Unset Git global parameter : #{key}")
begin
_, _, code = sudo_shell('git', 'config', '--global', '--unset', key)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
if code == 5
return true
else
logger.error("Error while removing Git global parameter : #{key}")
logger.error(e.output)
return false
end
end
end
def sudo_set_git_global_param(namespace, key, value)
key = prefix_key(namespace, key)
return sudo_unset_git_global_param(key) if value == ''
logger.info("Set Git global parameter : #{key} (#{value})")
begin
sudo_git('config', '--global', key, value)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Error while setting Git global parameter : #{key} (#{value})")
logger.error(e.output)
return false
end
end
# Return a hash with global config parameters.
def sudo_get_git_global_params(namespace)
begin
params = sudo_git('config', '--get-regexp', namespace).split("\n")
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Problems to retrieve Gitolite hook parameters in Gitolite config 'namespace : #{namespace}'")
params = []
end
git_config_as_hash(namespace, params)
end
def git_version
begin
sudo_git('--version')
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Can't retrieve Git version: #{e.output}")
'unknown'
end
end
private
# Return the Git command with prepend args (mainly env vars like FOO=BAR git push).
#
def git(args = [])
[*args, Repository::Xitolite.scm_command]
end
def git_args_for_repo(repo_path)
['--git-dir', repo_path]
end
# Returns the global gitconfig prefix for
# a config with that given key under the
# hooks namespace.
#
def prefix_key(namespace, key)
[namespace, '.', key].join
end
def git_config_as_hash(namespace, params)
value_hash = {}
params.each do |value_pair|
next if value_pair.empty?
next if !value_pair.start_with?(namespace)
global_key = value_pair.split(' ')[0]
value = value_pair.split(' ')[1]
key = global_key.split('.')[1]
value_hash[key] = value
end
value_hash
end
end
end
end

View file

@ -0,0 +1,81 @@
module RedmineGitHosting
module Commands
module Gitolite
extend self
#################################
# #
# Sudo+Gitolite Shell Wrapper #
# #
#################################
def gitolite_infos
ssh_capture('info')
end
def sudo_gitolite_query_rc(param)
sudo_capture('gitolite', 'query-rc', param).try(:chomp)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Can't retrieve Gitolite param : #{e.output}")
nil
end
def sudo_update_gitolite!
if gitolite_command.nil?
logger.error("gitolite_command is nil, can't update Gitolite !")
return
end
logger.info("Running '#{gitolite_command.join(' ')}' on the Gitolite install ...")
begin
sudo_shell(*gitolite_command)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
return false
end
end
def gitolite_repository_count
sudo_capture('gitolite', 'list-phy-repos').split("\n").length
end
# Test if repository is empty on Gitolite side
#
def sudo_repository_empty?(repo_path)
if gitolite_home_dir.nil?
logger.info('gitolite_home_dir is not set, because of incomplete/incorrect gitolite setup')
return true
end
repo_path = File.join(gitolite_home_dir, repo_path, 'objects')
count = sudo_git_objects_count(repo_path)
count.to_i.zero?
end
def sudo_git_objects_count(repo_path)
cmd = if RedmineGitHosting::Config.gitolite_use_sudo?
['eval', 'find', repo_path, '-type', 'f', '|', 'wc', '-l']
else
['bash', '-c', "find #{repo_path} -type f | wc -l"]
end
begin
sudo_capture(*cmd)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Can't retrieve Git objects count : #{e.output}")
0
end
end
private
def gitolite_command
RedmineGitHosting::Config.gitolite_command
end
def gitolite_home_dir
RedmineGitHosting::Config.gitolite_home_dir
end
end
end
end

View file

@ -0,0 +1,58 @@
module RedmineGitHosting
module Commands
module Ssh
extend self
# Return only the output from the ssh command.
#
def ssh_capture(*params)
cmd = ssh.concat(params)
capture(cmd)
end
# Execute a command in the gitolite forced environment through this user
# i.e., executes 'ssh git@localhost <command>'
#
# Returns stdout, stderr and the exit code
#
def ssh_shell(*params)
cmd = ssh.concat(params)
execute(cmd)
end
private
# Return the SSH command with basic args
#
def ssh
['ssh', *ssh_shell_params]
end
# Returns the ssh prefix arguments for all ssh_* commands.
#
# These are as follows:
# * (-T) Never request tty
# * (-i <gitolite_ssh_private_key>) Use the SSH keys given in Settings
# * (-p <gitolite_server_port>) Use port from settings
# * (-o BatchMode=yes) Never ask for a password
# * <gitolite_user>@<gitolite_server_host> (see +gitolite_url+)
#
def ssh_shell_params
[
'-T',
'-o', 'BatchMode=yes',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'StrictHostKeyChecking=no',
'-p', RedmineGitHosting::Config.gitolite_server_port,
'-i', RedmineGitHosting::Config.gitolite_ssh_private_key,
RedmineGitHosting::Config.gitolite_url
]
end
end
end
end

View file

@ -0,0 +1,227 @@
require 'digest/md5'
module RedmineGitHosting
module Commands
module Sudo
extend self
##########################
# #
# SUDO Shell Wrapper #
# #
##########################
# Pipe file content via sudo to dest_file.
# Expect file content to end with EOL (\n)
#
def sudo_install_file(content, dest_file, filemode)
stdin = [ 'cat', '<<\EOF', '>' + dest_file, "\n" + content.to_s + "EOF" ].join(' ')
begin
sudo_pipe_data(stdin)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
return false
else
begin
sudo_chmod(filemode, dest_file)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
return false
end
end
end
# Test if a file exists with size > 0
#
def sudo_file_exists?(filename)
sudo_test(filename, '-s')
end
# Test if a directory exists
#
def sudo_dir_exists?(dirname)
sudo_test(dirname, '-r')
end
# Test properties of a path from the git user.
#
# e.g., Test if a directory exists: sudo_test('~/somedir', '-d')
#
def sudo_test(path, testarg)
_, _ , code = sudo_shell('test', testarg, path)
return code == 0
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.debug("File check for #{path} failed : #{e.message}")
false
end
# Calls mkdir with the given arguments on the git user's side.
#
# e.g., sudo_mkdir('-p', '/some/path')
#
def sudo_mkdir(*args)
sudo_shell('mkdir', *args)
end
# Syntaxic sugar for 'mkdir -p'
#
def sudo_mkdir_p(path)
sudo_mkdir('-p', path)
end
# Calls chmod with the given arguments on the git user's side.
#
# e.g., sudo_chmod('755', '/some/path')
#
def sudo_chmod(mode, file)
sudo_shell('chmod', mode, file)
end
# Removes a directory and all subdirectories below gitolite_user's $HOME.
#
# Assumes a relative path.
#
# If force=true, it will delete using 'rm -rf <path>', otherwise
# it uses rmdir
#
def sudo_rmdir(path, force = false)
if force
sudo_shell('rm', '-rf', path)
else
sudo_shell('rmdir', path)
end
end
# Syntaxic sugar for 'rm -rf' command
#
def sudo_rm_rf(path)
sudo_rmdir(path, true)
end
# Moves a file/directory to a new target.
#
def sudo_move(old_path, new_path)
sudo_shell('mv', old_path, new_path)
end
def sudo_get_dir_size(directory)
sudo_capture('du', '-sh', directory).split(' ')[0] rescue ''
end
def sudo_cat(file)
sudo_capture('cat', file) rescue ''
end
# Test if file content has changed
#
def sudo_file_changed?(source_file, dest_file)
hash_content(content_from_redmine_side(source_file)) != hash_content(content_from_gitolite_side(dest_file))
end
# Test if file permissions has changed
#
def sudo_file_perms_changed?(filemode, dest_file)
current_mode = sudo_capture('stat', '-c', "%a", dest_file)
current_mode.chomp != filemode
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
false
end
# Return only the output of the shell command.
# Throws an exception if the shell command does not exit with code 0.
#
def sudo_capture(*params)
cmd = sudo.concat(params)
capture(cmd)
end
# Execute a command as the gitolite user defined in +GitoliteWrapper.gitolite_user+.
#
# Will shell out to +sudo -n -u <gitolite_user> params+
#
def sudo_shell(*params)
cmd = sudo.concat(params)
execute(cmd)
end
# Write data on stdin and return the output of the shell command.
# Throws an exception if the shell command does not exit with code 0.
#
def sudo_pipe_data(stdin)
cmd = sudo.push('sh')
capture(cmd, { stdin_data: stdin, binmode: true })
end
private
# Return the Sudo command with basic args.
#
def sudo
if RedmineGitHosting::Config.gitolite_use_sudo?
['sudo', *sudo_shell_params]
else
[]
end
end
# Returns the sudo prefix to all sudo_* commands.
#
# These are as follows:
# * (-i) login as `gitolite_user` (setting ENV['HOME')
# * (-n) non-interactive
# * (-u `gitolite_user`) target user
#
def sudo_shell_params
['-n', '-u', RedmineGitHosting::Config.gitolite_user, '-i']
end
# Return a md5 hash of the string passed.
#
def hash_content(content)
Digest::MD5.hexdigest(content)
end
# Return the content of a local (Redmine side) file.
#
def content_from_redmine_side(file)
File.read(file)
rescue Errno::ENOENT => e
logger.error(e.message)
''
end
# Return the content of a file on Gitolite side.
#
def content_from_gitolite_side(destination_path)
sudo_cat(destination_path)
end
end
end
end

View file

@ -0,0 +1,21 @@
module RedmineGitHosting
module Config
GITHUB_ISSUE = 'https://github.com/jbox-web/redmine_git_hosting/issues'
GITHUB_WIKI = 'http://redmine-git-hosting.io/configuration/variables/'
GITOLITE_DEFAULT_CONFIG_FILE = 'gitolite.conf'
GITOLITE_IDENTIFIER_DEFAULT_PREFIX = 'redmine_'
extend Config::Base
extend Config::GitoliteAccess
extend Config::GitoliteBase
extend Config::GitoliteCache
extend Config::GitoliteConfigTests
extend Config::GitoliteHooks
extend Config::GitoliteInfos
extend Config::GitoliteNotifications
extend Config::GitoliteStorage
extend Config::Mirroring
extend Config::RedmineConfig
end
end

View file

@ -0,0 +1,97 @@
module RedmineGitHosting
module Config
module Base
extend self
###############################
## ##
## CONFIGURATION ACCESSORS ##
## ##
###############################
def get_setting(setting, bool = false)
if bool
Additionals.true? do_get_setting(setting)
else
do_get_setting(setting)
end
end
def reload_from_file!
## Get default config from init.rb
default_hash = Redmine::Plugin.find('redmine_git_hosting').settings[:default]
do_reload_config(default_hash)
end
def dump_settings
puts YAML.dump Redmine::Plugin.find('redmine_git_hosting').settings[:default]
end
private
def do_get_setting(setting)
setting = setting.to_sym
## Wrap this in a begin/rescue statement because Setting table
## may not exist on first migration
begin
value = Setting.plugin_redmine_git_hosting[setting]
rescue
value = Redmine::Plugin.find('redmine_git_hosting').settings[:default][setting]
else
## The Setting table exist but does not contain the value yet, fallback to default
value = Redmine::Plugin.find('redmine_git_hosting').settings[:default][setting] if value.nil?
end
value
end
def do_reload_config(default_hash)
## Refresh Settings cache
Setting.check_cache
## Get actual values
valuehash = (Setting.plugin_redmine_git_hosting).clone rescue {}
## Update!
changes = 0
default_hash.each do |key, value|
if valuehash[key] != value
console_logger.info("Changing '#{key}' : #{valuehash[key]} => #{value}")
valuehash[key] = value
changes += 1
end
end
if changes.zero?
console_logger.info('No changes necessary.')
else
commit_changes(valuehash)
end
end
def commit_changes(valuehash)
console_logger.info('Committing changes ... ')
begin
## Update Settings
Setting.plugin_redmine_git_hosting = valuehash
## Refresh Settings cache
Setting.check_cache
console_logger.info('Success!')
rescue => e
console_logger.error('Failure.')
console_logger.error(e.message)
end
end
def console_logger
RedmineGitHosting::ConsoleLogger
end
def file_logger
RedmineGitHosting.logger
end
end
end
end

View file

@ -0,0 +1,70 @@
module RedmineGitHosting
module Config
module GitoliteAccess
extend self
def gitolite_http_by_default?
get_setting(:gitolite_http_by_default)
end
def gitolite_daemon_by_default?
get_setting(:gitolite_daemon_by_default, true)
end
def gitolite_notify_by_default?
get_setting(:gitolite_notify_by_default, true)
end
def ssh_server_domain
get_setting(:ssh_server_domain)
end
def http_server_domain
get_setting(:http_server_domain)
end
def https_server_domain
get_setting(:https_server_domain)
end
def http_server_subdir
get_setting(:http_server_subdir)
end
def http_root_url
my_root_url(false)
end
def https_root_url
my_root_url(true)
end
def redmine_root_url
Redmine::Utils::relative_url_root
end
def my_root_url(ssl = false)
if ssl && https_server_domain != ''
server_domain = https_server_domain
else
server_domain = http_server_domain
end
# Remove any path from httpServer.
# No trailing /.
File.join(server_domain[/^[^\/]*/], redmine_root_url, '/')[0..-2]
end
end
end
end

View file

@ -0,0 +1,132 @@
require 'etc'
module RedmineGitHosting
module Config
module GitoliteBase
extend self
def check_cache
@gitolite_home_dir = nil
@mirroring_keys_installed = nil
@mirroring_public_key = nil
@gitolite_ssh_fingerprint = nil
end
def redmine_user
@redmine_user ||= (%x[whoami]).chomp.strip
end
def gitolite_use_sudo?
redmine_user != gitolite_user
end
def gitolite_home_dir
@gitolite_home_dir ||= Etc.getpwnam(gitolite_user).dir rescue nil
end
def gitolite_bin_dir
@gitolite_bin_dir ||= RedmineGitHosting::Commands.sudo_gitolite_query_rc('GL_BINDIR')
end
def gitolite_lib_dir
@gitolite_lib_dir ||= RedmineGitHosting::Commands.sudo_gitolite_query_rc('GL_LIBDIR')
end
def gitolite_user
get_setting(:gitolite_user)
end
def gitolite_server_host
get_setting(:gitolite_server_host)
end
def gitolite_server_port
get_setting(:gitolite_server_port)
end
def gitolite_ssh_private_key
get_setting(:gitolite_ssh_private_key)
end
def gitolite_ssh_public_key
get_setting(:gitolite_ssh_public_key)
end
def gitolite_ssh_public_key_fingerprint
@gitolite_ssh_fingerprint ||= RedmineGitHosting::Utils::Ssh.ssh_fingerprint(File.read(gitolite_ssh_public_key))
end
def gitolite_config_file
File.basename(get_setting(:gitolite_config_file))
end
def gitolite_config_dir
dirs = File.dirname(gitolite_config_file).split('/')
if dirs[0] != '.'
File.join('conf', *dirs)
else
'conf'
end
end
def gitolite_identifier_prefix
get_setting(:gitolite_identifier_prefix)
end
def gitolite_identifier_strip_user_id?
get_setting(:gitolite_identifier_strip_user_id, true)
end
def gitolite_key_subdir
'redmine_git_hosting'
end
def git_config_username
get_setting(:git_config_username)
end
def git_config_email
get_setting(:git_config_email)
end
def gitolite_temp_dir
get_setting(:gitolite_temp_dir)
end
def gitolite_url
[gitolite_user, '@', gitolite_server_host].join
end
def gitolite_admin_dir
File.join(gitolite_temp_dir, gitolite_user, 'gitolite-admin.git')
end
def gitolite_log_level
get_setting(:gitolite_log_level)
end
end
end
end

View file

@ -0,0 +1,27 @@
module RedmineGitHosting
module Config
module GitoliteCache
extend self
def gitolite_cache_max_time
get_setting(:gitolite_cache_max_time).to_i
end
def gitolite_cache_max_elements
get_setting(:gitolite_cache_max_elements).to_i
end
def gitolite_cache_max_size
get_setting(:gitolite_cache_max_size).to_i * 1024 * 1024
end
def gitolite_cache_adapter
get_setting(:gitolite_cache_adapter)
end
end
end
end

View file

@ -0,0 +1,95 @@
module RedmineGitHosting
module Config
module GitoliteConfigTests
extend self
###############################
## ##
## TEMP DIR ##
## ##
###############################
@temp_dir_path = nil
@previous_temp_dir_path = nil
def create_temp_dir
if @previous_temp_dir_path != gitolite_temp_dir
@previous_temp_dir_path = gitolite_temp_dir
@temp_dir_path = gitolite_admin_dir
end
if !File.directory?(@temp_dir_path)
file_logger.info("Create Gitolite Admin directory : '#{@temp_dir_path}'")
begin
FileUtils.mkdir_p @temp_dir_path
FileUtils.chmod 0700, @temp_dir_path
rescue => e
file_logger.error("Cannot create Gitolite Admin directory : '#{@temp_dir_path}'")
end
end
@temp_dir_path
end
@temp_dir_writeable = false
def temp_dir_writeable?(opts = {})
@temp_dir_writeable = false if opts.has_key?(:reset) && opts[:reset] == true
if !@temp_dir_writeable
file_logger.debug("Testing if Gitolite Admin directory '#{create_temp_dir}' is writeable ...")
mytestfile = File.join(create_temp_dir, 'writecheck')
if !File.directory?(create_temp_dir)
@temp_dir_writeable = false
else
begin
FileUtils.touch mytestfile
FileUtils.rm mytestfile
rescue => e
@temp_dir_writeable = false
else
@temp_dir_writeable = true
end
end
end
@temp_dir_writeable
end
###############################
## ##
## SUDO TESTS ##
## ##
###############################
## SUDO TEST1
def can_redmine_sudo_to_gitolite_user?
return true unless gitolite_use_sudo?
file_logger.info("Testing if Redmine user '#{redmine_user}' can sudo to Gitolite user '#{gitolite_user}'...")
result = execute_sudo_test(gitolite_user) do
RedmineGitHosting::Commands.sudo_capture('whoami')
end
result ? file_logger.info('OK!') : file_logger.error('Error while testing can_redmine_sudo_to_gitolite_user')
result
end
def execute_sudo_test(user, &block)
begin
test = yield if block_given?
rescue RedmineGitHosting::Error::GitoliteCommandException => e
return false
else
if test.match(/#{user}/)
return true
else
return false
end
end
end
end
end
end

View file

@ -0,0 +1,69 @@
module RedmineGitHosting
module Config
module GitoliteHooks
extend self
def gitolite_hooks_namespace
'redminegitolite'
end
def gitolite_hooks_url
[get_setting(:gitolite_hooks_url), '/githooks/post-receive/redmine'].join
end
def gitolite_hooks_debug
get_setting(:gitolite_hooks_debug, true)
end
def gitolite_hooks_are_asynchronous
get_setting(:gitolite_hooks_are_asynchronous, true)
end
def gitolite_overwrite_existing_hooks?
get_setting(:gitolite_overwrite_existing_hooks, true)
end
def gitolite_local_code_dir
@gitolite_local_code_dir ||= RedmineGitHosting::Commands.sudo_gitolite_query_rc('LOCAL_CODE')
end
def gitolite_hooks_dir
if gitolite_version == 3
File.join(gitolite_local_code_dir, 'hooks', 'common')
else
File.join(gitolite_home_dir, '.gitolite', 'hooks', 'common')
end
end
def check_hooks_install!
{
hook_files: RedmineGitHosting::GitoliteHooks.hooks_installed?,
global_params: RedmineGitHosting::GitoliteParams::GlobalParams.new.installed?,
mailer_params: RedmineGitHosting::GitoliteParams::MailerParams.new.installed?
}
end
def install_hooks!
{
hook_files: RedmineGitHosting::GitoliteHooks.install_hooks!,
global_params: RedmineGitHosting::GitoliteParams::GlobalParams.new.install!,
mailer_params: RedmineGitHosting::GitoliteParams::MailerParams.new.install!
}
end
def update_hook_params!
RedmineGitHosting::GitoliteParams::GlobalParams.new.install!
end
end
end
end

View file

@ -0,0 +1,87 @@
module RedmineGitHosting
module Config
module GitoliteInfos
extend self
##########################
# #
# GITOLITE INFOS #
# #
##########################
def rugged_features
Rugged.features
end
def rugged_mandatory_features
[:threads, :ssh]
end
def libgit2_version
Rugged.libgit2_version.join('.')
end
def gitolite_infos
begin
RedmineGitHosting::Commands.gitolite_infos
rescue RedmineGitHosting::Error::GitoliteCommandException => e
file_logger.error('Error while getting Gitolite infos, check your SSH keys (path, permissions) or your Git user.')
nil
end
end
def gitolite_version
file_logger.debug('Getting Gitolite version...')
@gitolite_version ||= find_version(gitolite_infos)
end
def gitolite_banner
file_logger.debug('Getting Gitolite banner...')
gitolite_infos
end
def find_version(output)
return nil if output.blank?
line = output.split("\n")[0]
if line =~ /gitolite[ -]v?2./
2
elsif line.include?('running gitolite3')
3
else
nil
end
end
def gitolite_command
if gitolite_version == 2
['gl-setup']
elsif gitolite_version == 3
['gitolite', 'setup']
else
nil
end
end
def gitolite_repository_count
return 'This is Gitolite v2, not implemented...' if gitolite_version != 3
file_logger.debug('Getting Gitolite physical repositories list...')
begin
RedmineGitHosting::Commands.gitolite_repository_count
rescue RedmineGitHosting::Error::GitoliteCommandException => e
file_logger.error('Error while getting Gitolite physical repositories list')
0
end
end
end
end
end

View file

@ -0,0 +1,27 @@
module RedmineGitHosting
module Config
module GitoliteNotifications
extend self
def gitolite_notify_global_prefix
get_setting(:gitolite_notify_global_prefix)
end
def gitolite_notify_global_sender_address
get_setting(:gitolite_notify_global_sender_address)
end
def gitolite_notify_global_include
get_setting(:gitolite_notify_global_include)
end
def gitolite_notify_global_exclude
get_setting(:gitolite_notify_global_exclude)
end
end
end
end

View file

@ -0,0 +1,27 @@
module RedmineGitHosting
module Config
module GitoliteStorage
extend self
def gitolite_global_storage_dir
get_setting(:gitolite_global_storage_dir)
end
def gitolite_redmine_storage_dir
get_setting(:gitolite_redmine_storage_dir)
end
def gitolite_recycle_bin_dir
get_setting(:gitolite_recycle_bin_dir)
end
def recycle_bin_dir
File.join(gitolite_home_dir, gitolite_recycle_bin_dir) rescue nil
end
end
end
end

View file

@ -0,0 +1,22 @@
module RedmineGitHosting
module Config
module Mirroring
extend self
def mirroring_public_key
@mirroring_public_key ||= MirrorKeysInstaller.mirroring_public_key(gitolite_ssh_public_key)
end
def mirroring_keys_installed?
@mirroring_keys_installed ||= MirrorKeysInstaller.new(gitolite_home_dir, gitolite_ssh_public_key, gitolite_ssh_private_key).installed?
end
def gitolite_mirroring_script
File.join(gitolite_home_dir, '.ssh', 'run_gitolite_admin_ssh')
end
end
end
end

View file

@ -0,0 +1,55 @@
module RedmineGitHosting
module Config
module RedmineConfig
extend self
def gitolite_use_sidekiq?
get_setting(:gitolite_use_sidekiq, true)
end
def sidekiq_available?
@sidekiq_available ||=
begin
require 'sidekiq'
require 'sidekiq/api'
rescue LoadError
false
else
true
end
end
def hierarchical_organisation?
get_setting(:hierarchical_organisation, true)
end
def unique_repo_identifier?
get_setting(:unique_repo_identifier, true)
end
def all_projects_use_git?
get_setting(:all_projects_use_git, true)
end
def init_repositories_on_create?
get_setting(:init_repositories_on_create, true)
end
def show_repositories_url?
get_setting(:show_repositories_url, true)
end
def download_revision_enabled?
get_setting(:download_revision_enabled, true)
end
def delete_git_repositories?
get_setting(:delete_git_repositories, true)
end
def gitolite_recycle_bin_expiration_time
(get_setting(:gitolite_recycle_bin_expiration_time).to_f * 60).to_i
end
end
end
end

View file

@ -0,0 +1,37 @@
module RedmineGitHosting
module ConsoleLogger
extend self
def title(message, &block)
info("\n * #{message} :")
yield if block_given?
info(" Done !\n\n")
end
def debug(message)
puts message
logger.debug(message.strip)
end
def info(message)
puts message
logger.info(message.strip)
end
def warn
puts message
logger.warn(message.strip)
end
def error(message)
puts message
logger.error(message.strip)
end
private
def logger
RedmineGitHosting.logger
end
end
end

View file

@ -0,0 +1,24 @@
module RedmineGitHosting
module Error
# Used to register errors when pulling and pushing the conf file
class GitoliteException < StandardError; end
class GitoliteWrapperException < GitoliteException; end
class InvalidSshKey < GitoliteException; end
class InvalidRefspec < GitoliteException
class BadFormat < InvalidRefspec; end
class NullComponent < InvalidRefspec; end
end
# Used to register errors when pulling and pushing the conf file
class GitoliteCommandException < GitoliteException
attr_reader :command
attr_reader :output
def initialize(command, output)
@command = command
@output = output
end
end
end
end

View file

@ -0,0 +1,83 @@
module RedmineGitHosting
class GitAccess
DOWNLOAD_COMMANDS = %w[git-upload-pack git-upload-archive].freeze
PUSH_COMMANDS = %w[git-receive-pack].freeze
def download_access_check(actor, repository, is_ssl = false)
# First check that SmartHTTP is enabled for repository
return smart_http_is_disabled!(repository) unless smart_http_enabled_for_download?(repository, is_ssl)
# Then check user permissions
if actor.is_a?(User)
user_download_access_check(actor, repository)
else
raise 'Wrong actor'
end
end
def upload_access_check(actor, repository)
# First check that SmartHTTP is enabled for repository
return smart_http_is_disabled!(repository) unless smart_http_enabled_for_upload?(repository)
# Then check user permissions
if actor.is_a?(User)
user_upload_access_check(actor, repository)
else
raise 'Wrong actor'
end
end
def user_download_access_check(user, repository)
if user&.allowed_to_clone?(repository)
build_status_object(true)
else
build_status_object(false, "You don't have access")
end
end
def user_upload_access_check(user, repository)
if user&.allowed_to_commit?(repository)
build_status_object(true)
else
build_status_object(false, "You don't have access")
end
end
protected
def build_status_object(status, message = '')
logger.warn(message) unless status
GitAccessStatus.new(status, message)
end
private
def smart_http_is_disabled!(repository)
build_status_object(false, "SmartHTTP is disabled for repository '#{repository.gitolite_repository_name}' !")
end
def smart_http_enabled_for_download?(repository, is_ssl = false)
# SmartHTTP is disabled
return false unless repository.smart_http_enabled?
# HTTPS only but no SSL
return false if repository.only_https_access_enabled? && !is_ssl
# HTTP only but have SSL (weird..)
return false if repository.only_http_access_enabled? && is_ssl
# Else return true
true
end
def smart_http_enabled_for_upload?(repository)
# HTTPS only
return true if repository.pushable_via_http?
# Else
false
end
def logger
RedmineGitHosting.logger
end
end
end

View file

@ -0,0 +1,15 @@
module RedmineGitHosting
class GitAccessStatus
attr_accessor :status, :message
alias_method :allowed?, :status
def initialize(status, message = '')
@status = status
@message = message
end
def to_json
{ status: @status, message: @message }.to_json
end
end
end

View file

@ -0,0 +1,127 @@
module RedmineGitHosting
module GitoliteAccessor
extend self
module Methods
private
def gitolite_accessor
RedmineGitHosting::GitoliteAccessor
end
end
def create_ssh_key(ssh_key, opts = {})
logger.info("User '#{User.current.login}' has added a SSH key")
resync_gitolite(:add_ssh_key, ssh_key.id, opts)
end
def destroy_ssh_key(ssh_key, opts = {})
ssh_key = ssh_key.data_for_destruction if ssh_key.is_a?(GitolitePublicKey)
logger.info("User '#{User.current.login}' has deleted a SSH key")
resync_gitolite(:delete_ssh_key, ssh_key, opts)
end
def resync_ssh_keys(opts = {})
logger.info('Forced resync of all ssh keys...')
resync_gitolite(:resync_ssh_keys, 'all', opts)
end
def regenerate_ssh_keys(opts = {})
logger.info('Forced regenerate of all ssh keys...')
resync_gitolite(:regenerate_ssh_keys, 'all', opts)
end
def create_repository(repository, opts = {})
logger.info("User '#{User.current.login}' has created a new repository '#{repository.gitolite_repository_name}'")
resync_gitolite(:add_repository, repository.id, opts)
end
def update_repository(repository, opts = {})
logger.info("User '#{User.current.login}' has modified repository '#{repository.gitolite_repository_name}'")
resync_gitolite(:update_repository, repository.id, opts)
end
def move_repository(repository, opts = {})
logger.info("User '#{User.current.login}' has moved repository '#{repository.gitolite_repository_name}'")
resync_gitolite(:move_repository, repository.id, opts)
end
def destroy_repository(repository, opts = {})
logger.info("User '#{User.current.login}' has removed repository '#{repository.gitolite_repository_name}'")
resync_gitolite(:delete_repository, repository.data_for_destruction, opts)
end
def destroy_repositories(repositories, opts = {})
message = opts.delete(:message) { ' ' }
logger.info(message)
repositories.each do |repository|
resync_gitolite(:delete_repository, repository)
end
end
def update_projects(projects, opts = {})
message = opts.delete(:message) { ' ' }
logger.info(message)
resync_gitolite(:update_projects, projects, opts)
end
def move_project_hierarchy(project)
logger.info("Move repositories of project : '#{project}'")
resync_gitolite(:move_repositories, project.id)
end
def move_repositories_tree(count)
logger.info('Gitolite configuration has been modified : repositories hierarchy')
logger.info("Resync all projects (root projects : '#{count}')...")
resync_gitolite(:move_repositories_tree, count)
end
def purge_recycle_bin
resync_gitolite(:purge_recycle_bin, 'all')
end
def delete_from_recycle_bin(repositories)
resync_gitolite(:delete_from_recycle_bin, repositories)
end
def flush_git_cache
logger.info('Flush Git Cache !')
RedmineGitHosting::Cache.flush_cache!
end
def flush_settings_cache
resync_gitolite(:flush_settings_cache, 'flush!', { flush_cache: true })
end
def enable_rw_access
logger.info('Enable RW access on all Gitolite repositories')
resync_gitolite(:enable_rw_access, 'enable_rw_access')
end
def disable_rw_access
logger.info('Disable RW access on all Gitolite repositories')
resync_gitolite(:disable_rw_access, 'disable_rw_access')
end
private
def logger
RedmineGitHosting.logger
end
def resync_gitolite(command, object, options = {})
if options.has_key?(:bypass_sidekiq) && options[:bypass_sidekiq] == true
bypass = true
else
bypass = false
end
if RedmineGitHosting::Config.gitolite_use_sidekiq? &&
RedmineGitHosting::Config.sidekiq_available? && !bypass
GithostingShellWorker.maybe_do(command, object, options)
else
GitoliteWrapper.resync_gitolite(command, object, options)
end
end
end
end

View file

@ -0,0 +1,44 @@
module RedmineGitHosting
module GitoliteHandlers
module Repositories
class AddRepository < Base
def call
if !configuration_exists?
# Create repository in Gitolite
log_repo_not_exist('create it ...')
create_repository_config
elsif configuration_exists? && force
# Recreate repository in Gitolite
log_repo_already_exist('force mode !')
recreate_repository_config
else
log_repo_already_exist('exit !')
end
end
def gitolite_repo_name
repository.gitolite_repository_name
end
def gitolite_repo_path
repository.gitolite_repository_path
end
attr_reader :force
def initialize(*args)
super
@force = options.delete(:force) { false }
@old_perms = options.delete(:old_perms) { {} }
end
end
end
end
end

View file

@ -0,0 +1,129 @@
module RedmineGitHosting
module GitoliteHandlers
module Repositories
class Base
attr_reader :gitolite_config
attr_reader :repository
attr_reader :context
attr_reader :options
def initialize(gitolite_config, repository, context, options = {})
@gitolite_config = gitolite_config
@repository = repository
@context = context
@options = options.dup
@old_perms = {}
end
class << self
def call(gitolite_config, repository, context, options = {})
new(gitolite_config, repository, context, options).call
end
end
def call
raise NotImplementedError
end
private
def logger
RedmineGitHosting.logger
end
def backup_old_perms
@old_perms = repository.backup_gitolite_permissions(gitolite_repo_conf.permissions[0])
end
def configuration_exists?
!gitolite_repo_conf.nil?
end
def gitolite_repo_conf
gitolite_config.repos[gitolite_repo_name]
end
def create_repository_config
# Create Gitolite config
repo_conf = build_repository_config
# Update permissions
repo_conf.permissions = repository.build_gitolite_permissions(@old_perms)
# Add it to Gitolite
gitolite_config.add_repo(repo_conf)
# Return repository conf
repo_conf
end
def update_repository_config
recreate_repository_config
end
def delete_repository_config
gitolite_config.rm_repo(gitolite_repo_name)
end
def recreate_repository_config
# Backup old perms
backup_old_perms
# Remove repo from Gitolite conf, we're gonna recreate it
delete_repository_config
# Recreate repository in Gitolite
create_repository_config
end
def build_repository_config
repo_conf = ::Gitolite::Config::Repo.new(repository.gitolite_repository_name)
repository.git_config.each do |key, value|
repo_conf.set_git_config(key, value)
end
repository.gitolite_options.each do |key, value|
repo_conf.set_gitolite_option(key, value)
end
repo_conf
end
def log_ok_and_continue(message)
logger.info("#{context} : repository '#{gitolite_repo_name}' exists in Gitolite, #{message}")
logger.debug("#{context} : repository path '#{gitolite_repo_path}'")
end
def log_repo_not_exist(message)
logger.warn("#{context} : repository '#{gitolite_repo_name}' does not exist in Gitolite, #{message}")
logger.debug("#{context} : repository path '#{gitolite_repo_path}'")
end
def log_repo_already_exist(message)
logger.warn("#{context} : repository '#{gitolite_repo_name}' already exists in Gitolite, #{message}")
logger.debug("#{context} : repository path '#{gitolite_repo_path}'")
end
end
end
end
end

View file

@ -0,0 +1,30 @@
module RedmineGitHosting
module GitoliteHandlers
module Repositories
class DeleteRepository < Base
def call
if configuration_exists?
log_ok_and_continue('delete it ...')
# Delete Gitolite repository
delete_repository_config
else
log_repo_not_exist('exit !')
end
end
def gitolite_repo_name
repository[:repo_name]
end
def gitolite_repo_path
repository[:repo_path]
end
end
end
end
end

View file

@ -0,0 +1,207 @@
module RedmineGitHosting
module GitoliteHandlers
module Repositories
class MoveRepository < Base
def call
if configuration_exists?
perform_repository_move
else
logger.error("#{context} : repository '#{old_repo_name}' does not exist in Gitolite, exit !")
nil
end
end
def perform_repository_move
logger.info("#{context} : Moving '#{old_repo_name}' to '#{new_repo_name}' ...")
debug_output
if move_physical_repo(old_relative_path, new_relative_path, new_relative_parent_path)
# Update repository paths in database
update_repository_fields
# Update Gitolite configuration
update_gitolite
# Return old path to delete it
old_relative_parent_path
else
nil
end
end
private
def update_repository_fields
repository.update_column(:url, new_relative_path)
repository.update_column(:root_url, new_relative_path)
end
def update_gitolite
# Get old repository permissions
old_perms = repository.backup_gitolite_permissions(gitolite_repo_conf.permissions[0])
# Remove repository from Gitolite configuration
gitolite_config.rm_repo(old_repo_name)
# Recreate it
AddRepository.call(gitolite_config, repository, context, old_perms: old_perms)
end
def gitolite_repo_conf
@repo_conf ||= gitolite_config.repos[old_repo_name]
end
def repo_id
@repo_id ||= repository.redmine_name
end
def old_repo_name
@old_repo_name ||= repository.old_repository_name
end
def new_repo_name
@new_repo_name ||= repository.new_repository_name
end
def old_relative_path
@old_relative_path ||= repository.url
end
def new_relative_path
@new_relative_path ||= repository.gitolite_repository_path
end
def old_relative_parent_path
@old_relative_parent_path ||= old_relative_path.gsub(repo_id + '.git', '')
end
def new_relative_parent_path
@new_relative_parent_path ||= new_relative_path.gsub(repo_id + '.git', '')
end
def debug_output
logger.debug("#{context} : Old repository name (for Gitolite) : #{old_repo_name}")
logger.debug("#{context} : New repository name (for Gitolite) : #{new_repo_name}")
logger.debug("#{context} : Old relative path (for Redmine code browser) : #{old_relative_path}")
logger.debug("#{context} : New relative path (for Redmine code browser) : #{new_relative_path}")
logger.debug("#{context} : Old relative parent path (for Gitolite) : #{old_relative_parent_path}")
logger.debug("#{context} : New relative parent path (for Gitolite) : #{new_relative_parent_path}")
end
def move_physical_repo(old_path, new_path, new_parent_path)
## CASE 0
if old_path == new_path
logger.info("#{context} : old repository and new repository are identical '#{old_path}', nothing to do, exit !")
return true
end
# Now we have multiple options, due to the way gitolite sets up repositories
new_path_exists = directory_exists?(new_path)
old_path_exists = directory_exists?(old_path)
## CASE 1
if new_path_exists && old_path_exists
return move_physical_repo_case_1(old_path, new_path)
## CASE 2
elsif !new_path_exists && old_path_exists
return move_physical_repo_case_2(old_path, new_path, new_parent_path)
## CASE 3
elsif !new_path_exists && !old_path_exists
logger.error("#{context} : both old repository '#{old_path}' and new repository '#{new_path}' does not exist, cannot move it, exit but let Gitolite create the new repo !")
return true
## CASE 4
elsif new_path_exists && !old_path_exists
logger.error("#{context} : old repository '#{old_path}' does not exist, but the new one does, use it !")
return true
end
end
def move_physical_repo_case_1(old_path, new_path)
if empty_repository?(new_path)
logger.warn("#{context} : target repository '#{new_path}' already exists and is empty, remove it ...")
delete_directory!(new_path, :target)
else
logger.warn("#{context} : target repository '#{new_path}' exists and is not empty, considered as already moved, try to remove the old_path if empty")
if empty_repository?(old_path)
delete_directory!(old_path, :source)
else
logger.error("#{context} : the source repository directory is not empty, cannot remove it, exit ! (This repo will be orphan)")
false
end
end
end
def move_physical_repo_case_2(old_path, new_path, new_parent_path)
logger.debug("#{context} : really moving Gitolite repository from '#{old_path}' to '#{new_path}'")
create_parent_directory(new_parent_path) if !directory_exists?(new_parent_path)
begin
RedmineGitHosting::Commands.sudo_move(old_path, new_path)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("move_physical_repo(#{old_path}, #{new_path}) failed")
return false
else
logger.info("#{context} : done !")
return true
end
end
def delete_directory!(dir, type)
begin
RedmineGitHosting::Commands.sudo_rm_rf(dir)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("#{context} : removing existing #{type} repository failed, exit !")
return false
end
end
def empty_repository?(dir)
RedmineGitHosting::Commands.sudo_repository_empty?(dir)
end
def directory_exists?(dir)
RedmineGitHosting::Commands.sudo_dir_exists?(dir)
end
def create_parent_directory(new_parent_path)
begin
RedmineGitHosting::Commands.sudo_mkdir_p(new_parent_path)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("#{context} : creation of parent path '#{new_parent_path}' failed, exit !")
return false
end
end
end
end
end
end

View file

@ -0,0 +1,30 @@
module RedmineGitHosting
module GitoliteHandlers
module Repositories
class UpdateRepository < Base
def call
if configuration_exists?
log_ok_and_continue('update it ...')
# Update Gitolite repository
update_repository_config
else
log_repo_not_exist('exit !')
end
end
def gitolite_repo_name
repository.gitolite_repository_name
end
def gitolite_repo_path
repository.gitolite_repository_path
end
end
end
end
end

View file

@ -0,0 +1,25 @@
module RedmineGitHosting
module GitoliteHandlers
module SshKeys
class AddSshKey < Base
def call
repo_key = find_gitolite_key(key.owner, key.location)
# Add it if not found
if repo_key.nil?
admin.add_key(build_gitolite_key(key))
else
logger.info("#{context} : SSH key '#{key.owner}@#{key.location}' already exists in Gitolite, update it ...")
repo_key.type = key.type
repo_key.blob = key.blob
repo_key.email = key.email
repo_key.owner = key.owner
repo_key.location = key.location
end
end
end
end
end
end

View file

@ -0,0 +1,52 @@
module RedmineGitHosting
module GitoliteHandlers
module SshKeys
class Base
attr_reader :admin
attr_reader :key
attr_reader :context
def initialize(admin, key, context)
@admin = admin
@key = key
@context = context
end
class << self
def call(admin, key, context)
new(admin, key, context).call
end
end
def call
raise NotImplementedError
end
private
def logger
RedmineGitHosting.logger
end
def find_gitolite_key(owner, location)
admin.ssh_keys[owner].find_all { |k| k.location == location && k.owner == owner }.first
end
def build_gitolite_key(key)
::Gitolite::SSHKey.new(key.type, key.blob, key.email, key.owner, key.location)
end
end
end
end
end

View file

@ -0,0 +1,20 @@
module RedmineGitHosting
module GitoliteHandlers
module SshKeys
class DeleteSshKey < Base
def call
repo_key = find_gitolite_key(key[:owner], key[:location])
# Remove it if found
if repo_key
admin.rm_key(repo_key)
else
logger.info("#{context} : SSH key '#{key[:owner]}@#{key[:location]}' does not exits in Gitolite, exit !")
end
end
end
end
end
end

View file

@ -0,0 +1,156 @@
module RedmineGitHosting
class GitoliteHook
class << self
def def_field(*names)
class_eval do
names.each do |name|
define_method(name) do |*args|
args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
end
end
end
end
end
def_field :name, :source, :destination, :executable
attr_reader :source_dir
def initialize(source_dir, &block)
@source_dir = source_dir
instance_eval(&block)
end
def source_path
File.join(source_dir, source)
end
def destination_path
File.join(gitolite_hooks_dir, destination)
end
def parent_path
dirname = File.dirname(destination)
dirname = '' if dirname == '.'
File.join(gitolite_hooks_dir, dirname)
end
def filemode
executable ? '755' : '644'
end
def installed?
if !file_exists?
1
elsif hook_file_has_changed?
2
else
0
end
end
def install!
if !file_exists?
logger.info("Hook '#{name}' does not exist, installing it ...")
install_hook
elsif hook_file_has_changed?
logger.warn("Hook '#{name}' is already present but it's not ours!")
if force_update?
logger.info("Restoring '#{name}' hook since forceInstallHook == true")
install_hook
else
logger.info("Leaving '#{name}' hook untouched since forceInstallHook == false")
end
else
logger.info("Hook '#{name}' is correcly installed")
end
installed?
end
private
def install_hook
create_parent_dir if !directory_exists?
if install_hook_file
logger.info("Hook '#{name}' installed")
update_gitolite
end
end
def force_update?
RedmineGitHosting::Config.gitolite_overwrite_existing_hooks?
end
def logger
RedmineGitHosting.logger
end
def hook_file_has_changed?
RedmineGitHosting::Commands.sudo_file_changed?(source_path, destination_path) ||
RedmineGitHosting::Commands.sudo_file_perms_changed?(filemode, destination_path)
end
def file_exists?
RedmineGitHosting::Commands.sudo_file_exists?(destination_path)
end
def install_hook_file
logger.info("Installing hook '#{source_path}' in '#{destination_path}'")
begin
content = File.read(source_path)
rescue Errno::ENOENT => e
logger.error("Errors while installing hook '#{e.message}'")
return false
else
RedmineGitHosting::Commands.sudo_install_file(content, destination_path, filemode)
end
end
def update_gitolite
RedmineGitHosting::Commands.sudo_update_gitolite!
end
def gitolite_hooks_dir
RedmineGitHosting::Config.gitolite_hooks_dir
end
def directory_exists?
RedmineGitHosting::Commands.sudo_dir_exists?(parent_path)
end
def create_parent_dir
logger.info("Installing hook directory '#{parent_path}'")
begin
RedmineGitHosting::Commands.sudo_mkdir_p(parent_path)
return true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Problems installing hook directory '#{parent_path}'")
logger.error(e.output)
return false
end
end
end
end

View file

@ -0,0 +1,51 @@
module RedmineGitHosting
module GitoliteHooks
class << self
def register_hooks(&block)
@gitolite_hooks ||= []
class_eval(&block)
end
def registered_hooks
@gitolite_hooks
end
def source_dir(source_dir)
@source_dir = source_dir
end
def hooks_installed?
installed = {}
registered_hooks.each do |hook|
begin
installed[hook.name] = hook.installed?
rescue Exception => msg
installed[hook.name] = false
end
end
installed
end
def install_hooks!
installed = {}
registered_hooks.each do |hook|
installed[hook.name] = hook.install!
end
installed
end
def gitolite_hook(&block)
@gitolite_hooks << RedmineGitHosting::GitoliteHook.new(@source_dir, &block)
end
end
end
end

View file

@ -0,0 +1,25 @@
module RedmineGitHosting
module GitoliteParams
module BaseParam
private
# Return a hash with global config parameters.
#
def get_git_config_params(namespace)
RedmineGitHosting::Commands.sudo_get_git_global_params(namespace)
end
def set_git_config_param(namespace, key, value)
RedmineGitHosting::Commands.sudo_set_git_global_param(namespace, key, value)
end
def unset_git_config_param(key)
RedmineGitHosting::Commands.sudo_unset_git_global_param(key)
end
end
end
end

View file

@ -0,0 +1,49 @@
module RedmineGitHosting
module GitoliteParams
class GlobalParams
include BaseParam
attr_reader :gitolite_hooks_url
attr_reader :debug_mode
attr_reader :async_mode
attr_reader :namespace
attr_reader :current_params
def initialize
# Params to set
@gitolite_hooks_url = RedmineGitHosting::Config.gitolite_hooks_url
@debug_mode = RedmineGitHosting::Config.gitolite_hooks_debug.to_s
@async_mode = RedmineGitHosting::Config.gitolite_hooks_are_asynchronous.to_s
# Namespace where to set params
@namespace = RedmineGitHosting::Config.gitolite_hooks_namespace
# Get current params
@current_params = get_git_config_params(@namespace)
# Build hash of installed params
@installed = {}
end
def installed?
@installed['redmineurl'] = (current_params['redmineurl'] == gitolite_hooks_url)
@installed['debugmode'] = (current_params['debugmode'] == debug_mode)
@installed['asyncmode'] = (current_params['asyncmode'] == async_mode)
@installed
end
def install!
@installed['redmineurl'] = set_git_config_param(namespace, 'redmineurl', gitolite_hooks_url)
@installed['debugmode'] = set_git_config_param(namespace, 'debugmode', debug_mode)
@installed['asyncmode'] = set_git_config_param(namespace, 'asyncmode', async_mode)
@installed
end
end
end
end

View file

@ -0,0 +1,76 @@
module RedmineGitHosting
module GitoliteParams
class MailerParams
include BaseParam
attr_reader :namespace
attr_reader :current_params
attr_reader :current_mailer_params
def initialize
## Namespace where to set params
@namespace = 'multimailhook'
## Get current params
@current_params = get_git_config_params(@namespace)
@current_mailer_params = get_mailer_params
# Build hash of installed params
@installed = {}
end
def installed?
mailer_params.each do |param|
next if current_mailer_params[param].empty?
@installed[param] = (current_params[param] == current_mailer_params[param])
end
@installed
end
def install!
mailer_params.each do |param|
next if current_mailer_params[param].empty?
@installed[param] = set_git_config_param(namespace, param, current_mailer_params[param])
end
@installed
end
private
def mailer_params
%w(mailer environment smtpauth smtpserver smtpport smtpuser smtppass)
end
def get_mailer_params
params = {}
params['environment'] = 'gitolite'
params['mailer'] = mailer
params['smtpauth'] = smtpauth_enabled?.to_s
params['smtpserver'] = ActionMailer::Base.smtp_settings[:address].to_s
params['smtpport'] = ActionMailer::Base.smtp_settings[:port].to_s
params['smtpuser'] = ActionMailer::Base.smtp_settings[:user_name] || ''
params['smtppass'] = ActionMailer::Base.smtp_settings[:password] || ''
params
end
def mailer
ActionMailer::Base.delivery_method == :smtp ? 'smtp' : 'sendmail'
end
def smtpauth_enabled?
auth = ActionMailer::Base.smtp_settings[:authentication]
auth != nil && auth != '' && auth != :none
end
end
end
end

View file

@ -0,0 +1,109 @@
require 'gitolite'
module RedmineGitHosting
module GitoliteWrapper
extend self
# Update the Gitolite Repository
#
# action: An API action defined in one of the gitolite/* classes.
#
def resync_gitolite(action, object, options = {})
# Symbolize keys before using them
action = action.to_sym
options = options.symbolize_keys
# Flush cache if needed
flush_cache(options)
# Return if the action is only to flush cache on Sidekiq side
if action == :flush_settings_cache
logger.info('Settings cache flushed!')
return
else
execute_action(action, object, options)
end
end
private
def flush_cache(options = {})
if options.has_key?(:flush_cache) && options[:flush_cache] == true
logger.info('Flush Settings Cache !')
Setting.check_cache if Setting.respond_to?(:check_cache)
end
end
# Be sure to have a Gitolite::GitoliteAdmin object.
# Return if issues.
#
def execute_action(action, object, options = {})
begin
admin = gitolite_admin
rescue Rugged::SshError => e
logger.error 'Invalid Gitolite Admin SSH Keys'
logger.error(e.message)
rescue Rugged::NetworkError => e
logger.error 'Access denied for Gitolite Admin SSH Keys'
logger.error(e.message)
rescue Rugged::OSError => e
logger.error 'Invalid connection params'
logger.error(e.message)
rescue Rugged::RepositoryError => e
logger.error "Gitolite couldn't write to its admin repo copy"
logger.error "Try recreating '#{gitolite_admin_dir}'"
logger.error(e.message)
else
call_gitolite_wrapper(action, admin, object, options)
end
end
def gitolite_admin
RedmineGitHosting::Config.create_temp_dir
logger.debug("Accessing gitolite-admin.git at '#{gitolite_admin_dir}'")
::Gitolite::GitoliteAdmin.new(gitolite_admin_dir, gitolite_admin_settings)
end
def gitolite_admin_dir
RedmineGitHosting::Config.gitolite_admin_dir
end
def call_gitolite_wrapper(action, admin, object, options = {})
begin
klass = GitoliteWrappers::Base.find_by_action_name(action)
rescue RedmineGitHosting::Error::GitoliteWrapperException => e
logger.error(e.message)
else
klass.call(admin, object, options)
end
end
def gitolite_admin_settings
{
git_user: RedmineGitHosting::Config.gitolite_user,
hostname: "#{RedmineGitHosting::Config.gitolite_server_host}:#{RedmineGitHosting::Config.gitolite_server_port}",
host: "#{RedmineGitHosting::Config.gitolite_server_host}:#{RedmineGitHosting::Config.gitolite_server_port}",
author_name: RedmineGitHosting::Config.git_config_username,
author_email: RedmineGitHosting::Config.git_config_email,
public_key: RedmineGitHosting::Config.gitolite_ssh_public_key,
private_key: RedmineGitHosting::Config.gitolite_ssh_private_key,
key_subdir: RedmineGitHosting::Config.gitolite_key_subdir,
config_file: RedmineGitHosting::Config.gitolite_config_file,
config_dir: RedmineGitHosting::Config.gitolite_config_dir
}
end
def logger
RedmineGitHosting.logger
end
end
end

View file

@ -0,0 +1,112 @@
module RedmineGitHosting
module GitoliteWrappers
class Base
include RedmineGitHosting::GitoliteAccessor::Methods
attr_reader :admin
attr_reader :object_id
attr_reader :options
attr_reader :gitolite_config
def initialize(admin, object_id, options = {})
@admin = admin
@object_id = object_id
@options = options
@gitolite_config = admin.config
end
class << self
def call(admin, object_id, options = {})
new(admin, object_id, options).call
end
def inherited(klass)
@wrappers ||= {}
@wrappers[klass.name.demodulize.underscore.to_sym] = klass
end
def wrappers
@wrappers ||= {}
end
def find_by_action_name(action)
if wrappers.has_key?(action)
wrappers[action]
else
raise RedmineGitHosting::Error::GitoliteWrapperException.new("No available Wrapper for action '#{action}' found.")
end
end
end
def call
raise NotImplementedError
end
def gitolite_admin_repo_commit(message = '')
logger.info("#{context} : commiting to Gitolite...")
admin.save("#{context} : #{message}")
rescue => e
logger.error(e.message)
end
def create_gitolite_repository(repository)
GitoliteHandlers::Repositories::AddRepository.call(gitolite_config, repository, context, options)
end
def update_gitolite_repository(repository)
GitoliteHandlers::Repositories::UpdateRepository.call(gitolite_config, repository, context, options)
end
def delete_gitolite_repository(repository)
GitoliteHandlers::Repositories::DeleteRepository.call(gitolite_config, repository, context, options)
end
def move_gitolite_repository(repository)
GitoliteHandlers::Repositories::MoveRepository.call(gitolite_config, repository, context, options)
end
def create_gitolite_key(key)
GitoliteHandlers::SshKeys::AddSshKey.call(admin, key, context)
end
def delete_gitolite_key(key)
GitoliteHandlers::SshKeys::DeleteSshKey.call(admin, key, context)
end
private
def context
self.class.name.demodulize.underscore
end
def logger
RedmineGitHosting.logger
end
def log_object_dont_exist
logger.error("#{context} : repository does not exist anymore, object is nil, exit !")
end
end
end
end

View file

@ -0,0 +1,55 @@
module RedmineGitHosting
module GitoliteWrappers
module Global
module Common
def redmine_gitolite_key
'redmine_gitolite_admin_id_rsa'
end
def all_repository
'@all'
end
def all_repository_config
gitolite_config.repos[all_repository]
end
def rw_access_config
repo_conf = ::Gitolite::Config::Repo.new(all_repository)
repo_conf.permissions = rw_access_perms
repo_conf
end
def rw_access_perms
permissions = {}
permissions['RW+'] = {}
permissions['RW+'][''] = [redmine_gitolite_key]
[permissions]
end
def repo_conf
all_repository_config
end
def perms
repo_conf.permissions.select { |p| p.has_key? 'RW+' }
end
# RedmineGitHosting key can act on any refspec ('') so it should be in that 'subgroup'
#
def users
perms[0]['RW+']['']
end
end
end
end
end

View file

@ -0,0 +1,14 @@
module RedmineGitHosting
module GitoliteWrappers
module Global
class DeleteFromRecycleBin < GitoliteWrappers::Base
def call
RedmineGitHosting::RecycleBin.delete_content(object_id)
RedmineGitHosting.logger.info('delete_from_recycle_bin : done !')
end
end
end
end
end

View file

@ -0,0 +1,41 @@
module RedmineGitHosting
module GitoliteWrappers
module Global
class DisableRwAccess < GitoliteWrappers::Base
include Common
def call
if all_repository_config.nil?
logger.info("#{context} : RW access on all Gitolite repositories already disabled.")
return
else
admin.transaction do
remove_redmine_key
gitolite_admin_repo_commit('Disable RW access on all Gitolite repositories')
end
end
end
def remove_redmine_key
# RedmineGitHosting key must be in [RW+][''] group
# Return if those groups are absent : it means that our key is not here
return if perms.empty? || !perms[0]['RW+'].include?('')
# Check for key presence
return if !users.include?(redmine_gitolite_key)
# Delete the key
repo_conf.permissions[0]['RW+'][''].delete(redmine_gitolite_key)
# We cannot remove this repository as it may contains other configuration that we didn't check.
# Instead add a dummy key so the repo_conf is still valid for Gitolite
# RW+ = <empty string> is not valid
repo_conf.permissions[0]['RW+'][''].push('DUMMY_REDMINE_KEY') if repo_conf.permissions[0]['RW+'][''].empty?
end
end
end
end
end

View file

@ -0,0 +1,47 @@
module RedmineGitHosting
module GitoliteWrappers
module Global
class EnableRwAccess < GitoliteWrappers::Base
include Common
def call
if all_repository_config.nil?
admin.transaction do
gitolite_config.add_repo(rw_access_config)
gitolite_admin_repo_commit('Enable RW access on all Gitolite repositories')
end
else
logger.info("#{context} : '@all' repository already configured, check for RedmineGitHosting key presence")
admin.transaction do
add_redmine_key
gitolite_admin_repo_commit('Enable RW access on all Gitolite repositories')
end
end
end
def add_redmine_key
# RedmineGitHosting key must be in RW+ group
# If not create the RW+ group and add the key
if perms.empty?
logger.info("#{context} : No permissions set for '@all' repository, add RedmineGitHosting key")
repo_conf.permissions = rw_access_perms
elsif users.nil?
logger.info("#{context} : RedmineGitHosting key is not present, add it !")
repo_conf.permissions[0]['RW+'][''] = [redmine_gitolite_key]
elsif !users.include?(redmine_gitolite_key)
logger.info("#{context} : RedmineGitHosting key is not present, add it !")
repo_conf.permissions[0]['RW+'][''].push(redmine_gitolite_key)
else
logger.info("#{context} : RedmineGitHosting key is present, nothing to do.")
end
# Delete DUMMY_REDMINE_KEY if present
repo_conf.permissions[0]['RW+'][''].delete('DUMMY_REDMINE_KEY') if repo_conf.permissions[0]['RW+'][''].include?('DUMMY_REDMINE_KEY')
end
end
end
end
end

View file

@ -0,0 +1,14 @@
module RedmineGitHosting
module GitoliteWrappers
module Global
class PurgeRecycleBin < GitoliteWrappers::Base
def call
RedmineGitHosting::RecycleBin.delete_expired_content
RedmineGitHosting.logger.info('purge_recycle_bin : done !')
end
end
end
end
end

View file

@ -0,0 +1,39 @@
module RedmineGitHosting
module GitoliteWrappers
module Projects
module Common
def handle_repositories_move(projects)
repo_list = []
delete_parent_path = []
projects.reverse.each do |project|
project.gitolite_repos.reverse.each do |repository|
repo_list << repository.gitolite_repository_name
delete_parent_path << move_gitolite_repository(repository)
end
gitolite_admin_repo_commit("#{context} : #{project.identifier} | #{repo_list}")
end
delete_parent_path
end
def clean_path(path_list)
path_list.compact.uniq.sort.reverse.each do |path|
rmdir(path)
end
end
def rmdir(path)
logger.info("#{context} : cleaning repository path : '#{path}'")
begin
RedmineGitHosting::Commands.sudo_rmdir(path)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("#{context} : error while cleaning repository path '#{path}'")
end
end
end
end
end
end

View file

@ -0,0 +1,31 @@
module RedmineGitHosting
module GitoliteWrappers
module Projects
class MoveRepositories < GitoliteWrappers::Base
include Common
def call
return if git_projects.empty?
admin.transaction do
@delete_parent_path = []
@delete_parent_path += handle_repositories_move(git_projects)
# Remove empty directories
clean_path(@delete_parent_path)
end
end
def git_projects
@git_projects ||= projects.uniq.select { |p| p.gitolite_repos.any? }
end
def projects
@projects ||= Project.find_by_id(object_id).self_and_descendants
end
end
end
end
end

View file

@ -0,0 +1,32 @@
module RedmineGitHosting
module GitoliteWrappers
module Projects
class MoveRepositoriesTree < GitoliteWrappers::Base
include Common
# Move repositories tree in a single transaction
#
def call
admin.transaction do
@delete_parent_path = []
projects.each do |project|
# Only take projects that have Git repos.
git_projects = project.self_and_descendants.uniq.select { |p| p.gitolite_repos.any? }
next if git_projects.empty?
@delete_parent_path += handle_repositories_move(git_projects)
end
# Remove empty directories
clean_path(@delete_parent_path)
end
end
def projects
@projects ||= Project.includes(:repositories).all.select { |x| x.parent_id.nil? }
end
end
end
end
end

View file

@ -0,0 +1,48 @@
module RedmineGitHosting
module GitoliteWrappers
module Projects
class UpdateProjects < GitoliteWrappers::Base
def call
return if git_projects.empty?
admin.transaction do
git_projects.each do |project|
if project.gitolite_repos.any?
handle_project_update(project)
gitolite_admin_repo_commit(project.identifier)
end
end
end
end
def git_projects
@git_projects ||= projects.uniq.select { |p| p.gitolite_repos.any? }
end
def projects
@projects ||=
case object_id
when 'all'
Project.includes(:repositories).all
when 'active'
Project.active.includes(:repositories).all
when 'active_or_closed'
Project.active_or_closed.includes(:repositories).all
else
object_id.map { |project_id| Project.find_by_id(project_id) }
end
end
def handle_project_update(project)
project.gitolite_repos.each do |repository|
options[:force] == true ? create_gitolite_repository(repository) : update_gitolite_repository(repository)
end
end
end
end
end
end

View file

@ -0,0 +1,47 @@
module RedmineGitHosting
module GitoliteWrappers
module Repositories
class AddRepository < GitoliteWrappers::Base
def call
if !repository.nil?
create_repository
else
log_object_dont_exist
end
end
def repository
@repository ||= Repository.find_by_id(object_id)
end
def create_repository
admin.transaction do
create_gitolite_repository(repository)
gitolite_admin_repo_commit(repository.gitolite_repository_name)
@recovered = RedmineGitHosting::RecycleBin.restore_object_from_recycle(repository.gitolite_repository_name, repository.gitolite_full_repository_path)
if !@recovered
logger.info("#{context} : let Gitolite create empty repository '#{repository.gitolite_repository_path}'")
else
logger.info("#{context} : restored existing Gitolite repository '#{repository.gitolite_repository_path}' for update")
end
end
# Call Gitolite plugins
logger.info('Execute Gitolite Plugins')
# Create README file or initialize GitAnnex
RedmineGitHosting::Plugins.execute(:post_create, repository, options.merge(recovered: @recovered))
# Fetch changeset
repository.fetch_changesets
end
end
end
end
end

View file

@ -0,0 +1,36 @@
module RedmineGitHosting
module GitoliteWrappers
module Repositories
class DeleteRepository < GitoliteWrappers::Base
def call
if !repository.nil? && !repository.empty?
delete_repository
else
log_object_dont_exist
end
end
def repository
@repository ||= object_id.symbolize_keys
end
def delete_repository
admin.transaction do
delete_gitolite_repository(repository)
gitolite_admin_repo_commit(repository[:repo_name])
end
# Call Gitolite plugins
logger.info('Execute Gitolite Plugins')
# Move repository to RecycleBin
RedmineGitHosting::Plugins.execute(:post_delete, repository)
end
end
end
end
end

View file

@ -0,0 +1,33 @@
module RedmineGitHosting
module GitoliteWrappers
module Repositories
class MoveRepository < GitoliteWrappers::Base
def call
if !repository.nil?
move_repository
else
log_object_dont_exist
end
end
def repository
@repository ||= Repository.find_by_id(object_id)
end
def move_repository
admin.transaction do
move_gitolite_repository(repository)
gitolite_admin_repo_commit(repository.gitolite_repository_name)
end
# Fetch changeset
repository.fetch_changesets
end
end
end
end
end

View file

@ -0,0 +1,39 @@
module RedmineGitHosting
module GitoliteWrappers
module Repositories
class UpdateRepository < GitoliteWrappers::Base
def call
if !repository.nil?
update_repository
else
log_object_dont_exist
end
end
def repository
@repository ||= Repository.find_by_id(object_id)
end
def update_repository
admin.transaction do
update_gitolite_repository(repository)
gitolite_admin_repo_commit(repository.gitolite_repository_name)
end
# Call Gitolite plugins
logger.info('Execute Gitolite Plugins')
# Delete Git Config Keys
RedmineGitHosting::Plugins.execute(:post_update, repository, options)
# Fetch changeset
repository.fetch_changesets
end
end
end
end
end

View file

@ -0,0 +1,22 @@
module RedmineGitHosting
module GitoliteWrappers
module Users
class AddSshKey < GitoliteWrappers::Base
def call
logger.info("Adding SSH key '#{ssh_key.identifier}'")
admin.transaction do
create_gitolite_key(ssh_key)
gitolite_admin_repo_commit("Add SSH key : #{ssh_key.identifier}")
end
end
def ssh_key
@ssh_key ||= GitolitePublicKey.find_by_id(object_id)
end
end
end
end
end

View file

@ -0,0 +1,22 @@
module RedmineGitHosting
module GitoliteWrappers
module Users
class DeleteSshKey < GitoliteWrappers::Base
def call
logger.info("Deleting SSH key '#{ssh_key[:title]}'")
admin.transaction do
delete_gitolite_key(ssh_key)
gitolite_admin_repo_commit("Delete SSH key : #{ssh_key[:title]}")
end
end
def ssh_key
@ssh_key ||= object_id.symbolize_keys
end
end
end
end
end

View file

@ -0,0 +1,17 @@
module RedmineGitHosting
module GitoliteWrappers
module Users
class RegenerateSshKeys < GitoliteWrappers::Base
def call
GitolitePublicKey.all.each do |ssh_key|
gitolite_accessor.destroy_ssh_key(ssh_key, bypass_sidekiq: true)
ssh_key.reset_identifiers(skip_auto_increment: true)
gitolite_accessor.create_ssh_key(ssh_key, bypass_sidekiq: true)
end
end
end
end
end
end

View file

@ -0,0 +1,18 @@
module RedmineGitHosting
module GitoliteWrappers
module Users
class ResyncSshKeys < GitoliteWrappers::Base
def call
admin.transaction do
GitolitePublicKey.all.each do |ssh_key|
create_gitolite_key(ssh_key)
gitolite_admin_repo_commit("Add SSH key : #{ssh_key.identifier}")
end
end
end
end
end
end
end

View file

@ -0,0 +1,81 @@
require 'github/markup'
module RedmineGitHosting
class GitHostingHookListener < Redmine::Hook::ViewListener
render_on :view_projects_show_left, partial: 'projects/git_urls'
render_on :view_repository_edit_top, partial: 'repositories/edit_top'
render_on :view_repositories_show_contextual, partial: 'repositories/show_top'
render_on :view_repository_edit_bottom, partial: 'repositories/edit_bottom'
render_on :view_repositories_show_sidebar, partial: 'repositories/git_hosting_sidebar'
render_on :view_repositories_navigation, partial: 'repositories/git_hosting_navigation'
def view_layouts_base_html_head(_context = {})
header = ''
header << stylesheet_link_tag(:plugin, plugin: 'redmine_git_hosting') + "\n"
header << javascript_include_tag(:plugin, plugin: 'redmine_git_hosting') + "\n"
header
end
def view_my_account_contextual(context)
user = context[:user]
link_to(l(:label_my_public_keys), public_keys_path, class: 'icon icon-passwd') if user.allowed_to_create_ssh_keys?
end
def self.default_url_options
{ script_name: Redmine::Utils.relative_url_root }
end
REDMINE_MARKDOWN_EXT = %w[.txt].freeze
GITHUB_MARKDOWN_EXT = %w[.markdown .mdown .mkdn .md].freeze
def view_repositories_show_bottom(context)
path = get_path(context)
rev = get_rev(context)
repository = context[:repository]
readme_file = find_readme_file(repository, path, rev)
return '' if readme_file.nil?
content = get_formated_text(repository, readme_file, rev)
context[:controller].send(:render_to_string, partial: 'repositories/readme', locals: { html: content })
end
private
def get_path(context)
context[:request].params['path'] || ''
end
def get_rev(context)
rev = context[:request].params['rev']
rev.presence
end
def find_readme_file(repository, path, rev)
(repository.entries(path, rev) || []).find { |f| f.name =~ /README((\.).*)?/i }
end
def get_formated_text(repository, file, rev)
raw_readme_text = Redmine::CodesetUtil.to_utf8_by_setting(repository.cat(file.path, rev))
content =
if redmine_file?(file)
formatter_name = Redmine::WikiFormatting.format_names.find { |name| name =~ /markdown/i }
Redmine::WikiFormatting.formatter_for(formatter_name).new(raw_readme_text).to_html
elsif github_file?(file)
RedmineGitHosting::MarkdownRenderer.to_html(raw_readme_text)
else
GitHub::Markup.render(file.path, raw_readme_text)
end
content
end
def redmine_file?(file)
REDMINE_MARKDOWN_EXT.include?(File.extname(file.path))
end
def github_file?(file)
GITHUB_MARKDOWN_EXT.include?(File.extname(file.path))
end
end
end

View file

@ -0,0 +1,24 @@
require 'logger'
module RedmineGitHosting
class Logger < ::Logger
LOG_LEVELS = [
'debug',
'info',
'warn',
'error'
]
def self.init_logs!(appname, logfile, loglevel)
logger = new(logfile)
logger.progname = appname
logger.level = loglevel
logger.formatter = proc do |severity, time, progname, msg|
"#{time} [#{severity}] #{msg}\n"
end
logger
end
end
end

View file

@ -0,0 +1,29 @@
require 'html/pipeline'
module RedmineGitHosting
module MarkdownRenderer
extend self
def to_html(markdown)
pipeline.call(markdown)[:output].to_s
end
private
def pipeline
HTML::Pipeline.new(filters)
end
def filters
[
RedmineGitHosting::RedcarpetFilter,
HTML::Pipeline::AutolinkFilter,
HTML::Pipeline::TableOfContentsFilter
]
end
end
end

View file

@ -0,0 +1,129 @@
module RedmineGitHosting
class MirrorKeysInstaller
attr_reader :gitolite_home_dir
attr_reader :gitolite_ssh_public_key
attr_reader :gitolite_ssh_private_key
GITOLITE_MIRRORING_KEYS_NAME = 'redmine_gitolite_admin_id_rsa_mirroring'
def initialize(gitolite_home_dir, gitolite_ssh_public_key, gitolite_ssh_private_key)
@gitolite_home_dir = gitolite_home_dir
@gitolite_ssh_public_key = gitolite_ssh_public_key
@gitolite_ssh_private_key = gitolite_ssh_private_key
end
class << self
def mirroring_public_key(gitolite_ssh_public_key)
begin
format_mirror_key(File.read(gitolite_ssh_public_key))
rescue => e
RedmineGitHosting.logger.error("Error while loading mirroring public key : #{e.output}")
nil
end
end
def format_mirror_key(key)
key = key.chomp.strip
key.split(/[\t ]+/)[0].to_s + ' ' + key.split(/[\t ]+/)[1].to_s
end
end
def installed?
installable? && install!
end
def installable?
return false if gitolite_home_dir.nil?
return false if gitolite_ssh_public_key_content.nil?
return false if gitolite_ssh_private_key_content.nil?
return true
end
def install!
logger.info('Installing Redmine Gitolite mirroring SSH keys ...')
installed = install_public_key && install_private_key && install_mirroring_script
logger.info('Done!')
installed
end
def install_public_key
install_file(gitolite_ssh_public_key_content, gitolite_ssh_public_key_dest_path, '644') do
logger.error("Failed to install Redmine Git Hosting mirroring SSH public key : #{e.output}")
end
end
def install_private_key
install_file(gitolite_ssh_private_key_content, gitolite_ssh_private_key_dest_path, '600') do
logger.error("Failed to install Redmine Git Hosting mirroring SSH private key : #{e.output}")
end
end
def install_mirroring_script
install_file(mirroring_script_content, RedmineGitHosting::Config.gitolite_mirroring_script, '700') do
logger.error("Failed to install Redmine Git Hosting mirroring script : #{e.output}")
end
end
private
def logger
RedmineGitHosting.logger
end
def mirroring_script_content
[
'#!/bin/sh', "\n",
'exec', 'ssh', '-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=no', '-i', gitolite_ssh_private_key_dest_path, '"$@"',
"\n"
].join(' ')
end
def gitolite_ssh_public_key_content
File.read(gitolite_ssh_public_key)
rescue => e
nil
end
def gitolite_ssh_private_key_content
File.read(gitolite_ssh_private_key)
rescue => e
nil
end
def gitolite_ssh_public_key_dest_path
File.join(gitolite_home_dir, '.ssh', "#{GITOLITE_MIRRORING_KEYS_NAME}.pub")
end
def gitolite_ssh_private_key_dest_path
File.join(gitolite_home_dir, '.ssh', GITOLITE_MIRRORING_KEYS_NAME)
end
def install_file(source, destination, perms, &block)
RedmineGitHosting::Commands.sudo_install_file(source, destination, perms)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
yield
false
end
end
end

View file

@ -0,0 +1,70 @@
require_dependency 'changeset'
module RedmineGitHosting
module Patches
module ChangesetPatch
def github_payload
data = {}
data[:id] = revision
data[:message] = comments
data[:timestamp] = committed_on
data[:author] = author_data
data[:added] = added_files
data[:modified] = modified_files
data[:removed] = removed_files
data[:url] = url_for_revision(revision)
data
end
def author_data
{ name: author_name, email: author_email }
end
def author_name
RedmineGitHosting::Utils::Git.author_name(committer)
end
def author_email
RedmineGitHosting::Utils::Git.author_email(committer)
end
def added_files
filechanges_by_action('A')
end
def modified_files
filechanges_by_action('M')
end
def removed_files
filechanges_by_action('D')
end
def filechanges_by_action(action)
filechanges.select { |c| c.action == action }.map(&:path)
end
def url_for_revision(revision)
Rails.application.routes.url_helpers.url_for(
controller: 'repositories', action: 'revision',
id: project, repository_id: repository.identifier_param, rev: revision,
only_path: false, host: Setting['host_name'], protocol: Setting['protocol']
)
end
end
end
end
unless Changeset.included_modules.include?(RedmineGitHosting::Patches::ChangesetPatch)
Changeset.send(:prepend, RedmineGitHosting::Patches::ChangesetPatch)
end

View file

@ -0,0 +1,144 @@
require 'grack/auth'
module RedmineGitHosting
module Patches
module GrackAuthPatch
def call(env)
@env = env
@request = Rack::Request.new(env)
@auth = Rack::Auth::Basic::Request.new(env)
# Need this patch due to the rails mount
# Need this if under RELATIVE_URL_ROOT
# unless Gitlab.config.gitlab.relative_url_root.empty?
# # If website is mounted using relative_url_root need to remove it first
# @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'')
# else
# @env['PATH_INFO'] = @request.path
# end
if repository
auth!
else
render_not_found
end
end
private
def auth!
if @auth.provided?
return bad_request unless @auth.basic?
# Authentication with username and password
login, password = @auth.credentials
@user = authenticate_user(login, password)
@env['REMOTE_USER'] = @user.gitolite_identifier if @user
end
if authorized_request?
@app.call(@env)
else
unauthorized
end
end
def authenticate_user(login, password)
auth = RedmineGitHosting::Auth.new
auth.find(login, password)
end
def authorized_request?
case git_cmd
when *RedmineGitHosting::GitAccess::DOWNLOAD_COMMANDS
if @user
RedmineGitHosting::GitAccess.new.download_access_check(@user, repository, is_ssl?).allowed?
elsif repository.public_project? || repository.public_repo?
# Allow clone/fetch for public projects
true
else
false
end
when *RedmineGitHosting::GitAccess::PUSH_COMMANDS
# Push requires valid SSL
if !is_ssl?
logger.error('SmartHttp : your are trying to push data without SSL!, exiting !')
false
elsif @user
RedmineGitHosting::GitAccess.new.upload_access_check(@user, repository).allowed?
else
false
end
else
false
end
end
def git_cmd
if @request.get?
@request.params['service']
elsif @request.post?
File.basename(@request.path)
else
nil
end
end
def repository
@repository ||= repository_by_path(@request.path_info)
end
def repository_by_path(path)
if m = /([^\/]+\/)*?[^\/]+\.git/.match(path).to_a
repo_path = m.first
Repository::Xitolite.find_by_path(repo_path, loose: true)
end
end
def is_ssl?
@request.ssl? || https_headers? || x_forwarded_proto_headers? || x_forwarded_ssl_headers?
end
def https_headers?
@request.env['HTTPS'].to_s == 'on'
end
def x_forwarded_proto_headers?
@request.env['HTTP_X_FORWARDED_PROTO'].to_s == 'https'
end
def x_forwarded_ssl_headers?
@request.env['HTTP_X_FORWARDED_SSL'].to_s == 'on'
end
def render_not_found
[404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
end
def logger
RedmineGitHosting.logger
end
end
end
end
unless Grack::Auth.included_modules.include?(RedmineGitHosting::Patches::GrackAuthPatch)
Grack::Auth.send(:prepend, RedmineGitHosting::Patches::GrackAuthPatch)
end

View file

@ -0,0 +1,132 @@
require 'grack/git'
module RedmineGitHosting
module Patches
module GrackGitPatch
def initialize(git_path, repo_path, user)
@user = user
super(git_path, repo_path)
end
# Override original *valid_repo?* method to test directory presence with Sudo
def valid_repo?
directory_exists?(repo)
end
# Override original *command* method to prefix the command with Sudo and other args.
#
def command(cmd)
git_command_with_sudo(cmd)
end
# Override original *capture* method because the original IO.popen().read let zombie process behind.
# This method is called :
# * to get repository Git config (http.uploadpack || http.receivepack)
# * to get repository info refs :
# 0087deab8f3d612a47e7e153ed21bbc52a480205035a refs/heads/devel report-status delete-refs side-band-64k quiet ofs-delta agent=git/1.9.1
# * to get repository refs :
# 003f91a7b1dad21020e96d52119c585881c02f2fae45 refs/heads/master
# Note : *service_rpc* also calls IO.popen but pass a block !.
# Passing a block to IO.popen auto-close the pipe/thread.
def capture(command)
# Extract Args
cmd = command.shift
args = command
begin
RedmineGitHosting::Utils::Exec.capture(cmd, args)
rescue => e
logger.error('Problems while getting SmartHttp params')
# Return empty string since the next method will call *chomp* on it
''
end
end
# Override original *popen_options* method.
# The original one try to chdir before executing the command by
# passing 'chdir: @dir' option to IO.popen.
# This is wrong as we can't chdir to Gitolite directory.
# Notes : this method is called in *service_rpc* (not overriden)
#
def popen_options
{ unsetenv_others: true }
end
# Override original *popen_env* method.
# The original one passes useless arg (GL_ID) to IO.popen.
# Notes : this method is called in *service_rpc* (not overriden)
#
def popen_env
{ 'PATH' => ENV['PATH'] }
end
private
def directory_exists?(dir)
RedmineGitHosting::Commands.sudo_dir_exists?(dir)
end
# We sometimes need to add *--git-dir* arg to Git command otherwise
# Git looks for the repository in the current path.
def git_command_with_sudo(params)
if command_require_chdir?(params.last)
git_command_with_chdir.concat(params)
else
git_command_without_chdir.concat(params)
end
end
def command_require_chdir?(cmd)
cmd == 'update-server-info' || cmd == 'http.receivepack' || cmd == 'http.uploadpack' || cmd == 'rev-parse'
end
def git_command_without_chdir
RedmineGitHosting::Commands.sudo_git_cmd(smart_http_args)
end
def git_command_with_chdir
RedmineGitHosting::Commands.sudo_git_args_for_repo(@repo, smart_http_args)
end
def smart_http_args
[
'env',
"GL_BINDIR=#{RedmineGitHosting::Config.gitolite_bin_dir}",
"GL_LIBDIR=#{RedmineGitHosting::Config.gitolite_lib_dir}",
"GL_REPO=#{repository_object.gitolite_repository_name}",
"GL_USER=#{@user}"
]
end
def logger
RedmineGitHosting.logger
end
def repository_object
@repository_object ||= Repository::Xitolite.find_by_path(@repo, loose: true)
end
end
end
end
unless Grack::Git.included_modules.include?(RedmineGitHosting::Patches::GrackGitPatch)
Grack::Git.send(:prepend, RedmineGitHosting::Patches::GrackGitPatch)
end

View file

@ -0,0 +1,27 @@
require 'grack/server'
module RedmineGitHosting
module Patches
module GrackServerPatch
# Override original *get_git* method to set the right path for the repository.
# Also pass the *@env['REMOTE_USER']* variable to the Git constructor so we
# can pass it to Gitolite hooks later.
def get_git(path)
path = gitolite_path(path)
Grack::Git.new(@config[:git_path], path, @env['REMOTE_USER'])
end
private
def gitolite_path(path)
File.join(RedmineGitHosting::Config.gitolite_home_dir, RedmineGitHosting::Config.gitolite_global_storage_dir, RedmineGitHosting::Config.gitolite_redmine_storage_dir, path)
end
end
end
end
unless Grack::Server.included_modules.include?(RedmineGitHosting::Patches::GrackServerPatch)
Grack::Server.send(:prepend, RedmineGitHosting::Patches::GrackServerPatch)
end

View file

@ -0,0 +1,37 @@
require_dependency 'group'
module RedmineGitHosting
module Patches
module GroupPatch
def self.prepended(base)
base.class_eval do
# Relations
has_many :protected_branches_members, dependent: :destroy, foreign_key: :principal_id
has_many :protected_branches, through: :protected_branches_members
end
end
def user_added(user)
super
protected_branches.each do |pb|
RepositoryProtectedBranches::MemberManager.new(pb).add_user_from_group(user, self.id)
end
end
def user_removed(user)
super
protected_branches.each do |pb|
RepositoryProtectedBranches::MemberManager.new(pb).remove_user_from_group(user, self.id)
end
end
end
end
end
unless Group.included_modules.include?(RedmineGitHosting::Patches::GroupPatch)
Group.send(:prepend, RedmineGitHosting::Patches::GroupPatch)
end

View file

@ -0,0 +1,19 @@
require_dependency 'issue'
module RedmineGitHosting
module Patches
module IssuePatch
def self.prepended(base)
base.class_eval do
has_one :github_issue, foreign_key: 'issue_id', class_name: 'GithubIssue', dependent: :destroy
end
end
end
end
end
unless Issue.included_modules.include?(RedmineGitHosting::Patches::IssuePatch)
Issue.send(:prepend, RedmineGitHosting::Patches::IssuePatch)
end

View file

@ -0,0 +1,19 @@
require_dependency 'journal'
module RedmineGitHosting
module Patches
module JournalPatch
def self.prepended(base)
base.class_eval do
has_one :github_comment, foreign_key: 'journal_id', class_name: 'GithubComment', dependent: :destroy
end
end
end
end
end
unless Journal.included_modules.include?(RedmineGitHosting::Patches::JournalPatch)
Journal.send(:prepend, RedmineGitHosting::Patches::JournalPatch)
end

View file

@ -0,0 +1,28 @@
require_dependency 'member'
module RedmineGitHosting
module Patches
module MemberPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def self.prepended(base)
base.class_eval do
after_commit :update_project
end
end
private
def update_project
options = { message: "Membership changes on project '#{project}', update!" }
gitolite_accessor.update_projects([project.id], options)
end
end
end
end
unless Member.included_modules.include?(RedmineGitHosting::Patches::MemberPatch)
Member.send(:prepend, RedmineGitHosting::Patches::MemberPatch)
end

View file

@ -0,0 +1,63 @@
require_dependency 'project'
module RedmineGitHosting
module Patches
module ProjectPatch
def self.prepended(base)
base.class_eval do
# Add custom scope
scope :active_or_closed, -> { where("status = #{Project::STATUS_ACTIVE} OR status = #{Project::STATUS_CLOSED}") }
# Make sure that identifier does not match Gitolite Admin repository
validates_exclusion_of :identifier, in: %w(gitolite-admin)
# Place additional constraints on repository identifiers because of multi repos
validate :additional_constraints_on_identifier
end
end
# Find all repositories owned by project which are Repository::Xitolite
def gitolite_repos
repositories.select { |x| x.is_a?(Repository::Xitolite) }.sort { |x, y| x.id <=> y.id }
end
# Return first repo with a blank identifier (should be only one!)
def repo_blank_ident
Repository.where("project_id = ? and (identifier = '' or identifier is null)", id).first
end
def users_available
get_members_available('User')
end
def groups_available
get_members_available('Group')
end
private
def get_members_available(klass)
memberships.active.map(&:principal).select { |m| m.class.name == klass }.uniq.sort
end
def additional_constraints_on_identifier
if new_record? && !identifier.blank?
# Make sure that identifier does not match existing repository identifier
errors.add(:identifier, :taken) if Repository.find_by_identifier_and_type(identifier, 'Repository::Xitolite')
end
end
end
end
end
unless Project.included_modules.include?(RedmineGitHosting::Patches::ProjectPatch)
Project.send(:prepend, RedmineGitHosting::Patches::ProjectPatch)
end

View file

@ -0,0 +1,132 @@
require_dependency 'projects_controller'
module RedmineGitHosting
module Patches
module ProjectsControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def self.prepended(base)
base.class_eval do
helper :bootstrap_kit
helper :additionals_clipboardjs
helper :extend_projects
end
end
def create
super
# Only create repo if project creation worked
create_project_repository if valid_project?
end
def update
super
if @project.gitolite_repos.detect { |repo| repo.url != repo.gitolite_repository_path || repo.url != repo.root_url }
# Hm... something about parent hierarchy changed. Update us and our children
move_project_hierarchy
else
update_project("Set Git daemon for repositories of project : '#{@project}'")
end
end
def destroy
# Build repositories list before project destruction.
repositories_list = repositories_to_destroy
# Destroy project
super
# Destroy repositories
destroy_repositories(repositories_list) if api_request? || params[:confirm]
end
def archive
super
update_project_hierarchy("User '#{User.current.login}' has archived project '#{@project}', update it !")
end
def unarchive
super
update_project("User '#{User.current.login}' has unarchived project '#{@project}', update it !")
end
def close
super
update_project_hierarchy("User '#{User.current.login}' has closed project '#{@project}', update it !")
end
def reopen
super
update_project_hierarchy("User '#{User.current.login}' has reopened project '#{@project}', update it !")
end
private
def valid_project?
if Rails::VERSION::MAJOR == 3
validate_parent_id && @project.save
else
@project.save
end
end
# Call UseCase object that will complete Project repository creation :
# it will create the Repository::Xitolite association, the GitExtra association and then
# the repository in Gitolite.
#
def create_project_repository
if @project.module_enabled?('repository') && RedmineGitHosting::Config.all_projects_use_git?
if Setting.enabled_scm.include?('Xitolite')
Projects::CreateRepository.call(@project)
else
flash[:error] = l(:error_xitolite_repositories_disabled)
end
end
end
def move_project_hierarchy
gitolite_accessor.move_project_hierarchy(@project)
end
def update_project(message)
options = { message: message }
Projects::Update.call(@project, options)
end
def update_project_hierarchy(message)
options = { message: message }
gitolite_accessor.update_projects(hierarchy_to_update, options)
end
def hierarchy_to_update
# Only take projects that have Git repos.
@project.self_and_descendants.uniq.select { |p| p.gitolite_repos.any? }.map(&:id)
end
def destroy_repositories(repositories_list)
options = { message: "User '#{User.current.login}' has destroyed project '#{@project}', delete all Gitolite repositories !" }
gitolite_accessor.destroy_repositories(repositories_list, options)
end
def repositories_to_destroy
destroy_repositories = []
# Get all projects hierarchy
projects = @project.self_and_descendants
# Only take projects that have Git repos.
git_projects = projects.uniq.select { |p| p.gitolite_repos.any? }
git_projects.reverse.each do |project|
project.gitolite_repos.reverse.each do |repository|
destroy_repositories << repository.data_for_destruction
end
end
destroy_repositories
end
end
end
end
unless ProjectsController.included_modules.include?(RedmineGitHosting::Patches::ProjectsControllerPatch)
ProjectsController.send(:prepend, RedmineGitHosting::Patches::ProjectsControllerPatch)
end

View file

@ -0,0 +1,162 @@
require_dependency 'repositories_controller'
module RedmineGitHosting
module Patches
module RepositoriesControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def self.prepended(base)
base.class_eval do
before_action :set_current_tab, only: :edit
helper :bootstrap_kit
helper :additionals_clipboardjs
helper :watchers
# Load ExtendRepositoriesHelper so we can call our
# additional methods.
helper :extend_repositories
end
end
def show
if @repository.is_a?(Repository::Xitolite) && @repository.empty?
# Fake list of repos
@repositories = @project.gitolite_repos
render 'git_instructions'
else
super
end
end
def create
super
call_use_cases
end
def update
super
call_use_cases
end
def destroy
super
call_use_cases
end
# Monkey patch *diff* method to pass the *bypass_cache* flag
# on diff download.
#
def diff
if @repository.is_a?(Repository::Xitolite)
diff_with_options
else
super
end
end
private
def set_current_tab
@tab = params[:tab] || ''
end
def call_use_cases
return unless @repository.is_a?(Repository::Xitolite)
return if @repository.errors.any?
case action_name
when 'create'
# Call UseCase object that will complete Repository creation :
# it will create GitExtra association and then the repository in Gitolite.
Repositories::Create.call(@repository, creation_options)
when 'update'
gitolite_accessor.update_repository(@repository)
when 'destroy'
gitolite_accessor.destroy_repository(@repository)
end
end
def creation_options
{ create_readme_file: create_readme_file?, enable_git_annex: enable_git_annex? }
end
def create_readme_file?
Additionals.true? @repository.create_readme
end
def enable_git_annex?
Additionals.true? @repository.enable_git_annex
end
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
# Monkey patch *find_project_repository* method to render Git instructions
# if repository has no branch
#
def find_project_repository
@project = Project.find(params[:id])
if params[:repository_id].present?
@repository = @project.repositories.find_by_identifier_param(params[:repository_id])
else
@repository = @project.repository
end
(render_404; return false) unless @repository
@path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
@rev_to = params[:rev_to]
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
raise InvalidRevisionParam if @repository.branches.empty?
end
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
# Fake list of repos
@repositories = @project.gitolite_repos
render 'git_instructions'
end
# This is the original diff method with the *bypass_cache* flag
# for diff download. We keep the cache for the diff view.
#
def diff_with_options
if params[:format] == 'diff'
@diff = @repository.diff(@path, @rev, @rev_to, bypass_cache: true)
(show_error_not_found; return) unless @diff
filename = "changeset_r#{@rev}"
filename << "_r#{@rev_to}" if @rev_to
send_data @diff.join, filename: "#{filename}.diff",
type: 'text/x-patch',
disposition: 'attachment'
else
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
@diff_type = 'inline' unless %w[inline sbs].include?(@diff_type)
# Save diff type as user preference
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
User.current.pref[:diff_type] = @diff_type
User.current.preference.save
end
@cache_key = "repositories/diff/#{@repository.id}/" +
Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
unless read_fragment(@cache_key)
@diff = @repository.diff(@path, @rev, @rev_to)
unless @diff
show_error_not_found
return
end
end
@changeset = @repository.find_changeset_by_name(@rev)
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
@diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
render :diff, formats: :html
end
end
end
end
end
unless RepositoriesController.included_modules.include?(RedmineGitHosting::Patches::RepositoriesControllerPatch)
RepositoriesController.send(:prepend, RedmineGitHosting::Patches::RepositoriesControllerPatch)
end

View file

@ -0,0 +1,19 @@
require_dependency 'repositories_helper'
module RedmineGitHosting
module Patches
module RepositoriesHelperPatch
def xitolite_field_tags(form, repository)
encoding_field(form, repository) +
create_readme_field(form, repository) +
enable_git_annex_field(form, repository)
end
end
end
end
unless RepositoriesHelper.included_modules.include?(RedmineGitHosting::Patches::RepositoriesHelperPatch)
RepositoriesHelper.send(:prepend, RedmineGitHosting::Patches::RepositoriesHelperPatch)
end

View file

@ -0,0 +1,19 @@
require_dependency 'repository'
module RedmineGitHosting
module Patches
module RepositoryPatch
# This is the (possibly non-unique) basename for the Gitolite repository
#
def redmine_name
identifier.blank? ? project.identifier : identifier
end
end
end
end
unless Repository.included_modules.include?(RedmineGitHosting::Patches::RepositoryPatch)
Repository.send(:prepend, RedmineGitHosting::Patches::RepositoryPatch)
end

View file

@ -0,0 +1,47 @@
require_dependency 'roles_controller'
module RedmineGitHosting
module Patches
module RolesControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def create
super
call_gitolite('created')
end
def update
super
call_gitolite('modified')
end
def destroy
super
call_gitolite('deleted')
end
def permissions
super
call_gitolite('modified') if request.post?
end
private
def call_gitolite(message)
options = { message: "Role has been #{message}, resync all projects (active or closed)..." }
gitolite_accessor.update_projects('active_or_closed', options)
end
end
end
end
unless RolesController.included_modules.include?(RedmineGitHosting::Patches::RolesControllerPatch)
RolesController.send(:prepend, RedmineGitHosting::Patches::RolesControllerPatch)
end

View file

@ -0,0 +1,32 @@
require_dependency 'setting'
module RedmineGitHosting
module Patches
module SettingPatch
def self.prepended(base)
class << base
prepend ClassMethods
end
end
module ClassMethods
def check_cache
settings_updated_on = Setting.maximum(:updated_on)
if settings_updated_on && @cached_cleared_on <= settings_updated_on
clear_cache
RedmineGitHosting::Config.check_cache
end
end
end
end
end
end
unless Setting.included_modules.include?(RedmineGitHosting::Patches::SettingPatch)
Setting.send(:prepend, RedmineGitHosting::Patches::SettingPatch)
end

View file

@ -0,0 +1,76 @@
require_dependency 'settings_controller'
module RedmineGitHosting
module Patches
module SettingsControllerPatch
def self.prepended(base)
base.class_eval do
helper :bootstrap_kit
helper :gitolite_plugin_settings
end
end
def authors
@plugin = Redmine::Plugin.find(params[:id])
return render_404 unless @plugin.id == :redmine_git_hosting
@authors = RedmineGitHosting.authors
render layout: false
end
def install_gitolite_hooks
@plugin = Redmine::Plugin.find(params[:id])
return render_404 unless @plugin.id == :redmine_git_hosting
@gitolite_checks = RedmineGitHosting::Config.install_hooks!
end
def plugin
@plugin = Redmine::Plugin.find(params[:id])
return super unless @plugin.id == :redmine_git_hosting
if request.post?
handle_settings_update
redirect_to plugin_settings_path(@plugin)
else
@partial = @plugin.settings[:partial]
@settings = Setting.send "plugin_#{@plugin.id}"
end
end
private
def handle_settings_update
# Create FormObject
settings_form = PluginSettingsForm.new(@plugin)
# Strip *rescue* hash from params as we don't want to save them
options = params[:settings].delete(:rescue) { {} }
# Validate form
if settings_form.submit(params[:settings])
# Backup old settings
old_settings = Setting.send("plugin_#{@plugin.id}")
# Save settings for real
Setting.send "plugin_#{@plugin.id}=", settings_form.params
# Execute post actions
execute_post_actions(old_settings, options)
flash[:notice] = l(:notice_successful_update)
else
flash[:error] = settings_form.errors.full_messages.join('<br>')
end
end
def execute_post_actions(old_settings, opts = {})
Settings::Apply.call(old_settings, opts)
end
end
end
end
unless SettingsController.included_modules.include?(RedmineGitHosting::Patches::SettingsControllerPatch)
SettingsController.send(:prepend, RedmineGitHosting::Patches::SettingsControllerPatch)
end

View file

@ -0,0 +1,25 @@
require_dependency 'sys_controller'
module RedmineGitHosting
module Patches
module SysControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def fetch_changesets
# Flush GitCache
gitolite_accessor.flush_git_cache
super
# Purge RecycleBin
gitolite_accessor.purge_recycle_bin
end
end
end
end
unless SysController.included_modules.include?(RedmineGitHosting::Patches::SysControllerPatch)
SysController.send(:prepend, RedmineGitHosting::Patches::SysControllerPatch)
end

View file

@ -0,0 +1,105 @@
require_dependency 'user'
module RedmineGitHosting
module Patches
module UserPatch
def self.prepended(base)
base.class_eval do
# Virtual attribute
attr_accessor :status_has_changed
# Relations
has_many :gitolite_public_keys, dependent: :destroy
has_many :protected_branches_members, dependent: :destroy, foreign_key: :principal_id
has_many :protected_branches, through: :protected_branches_members
# Callbacks
after_save :check_if_status_changed
end
end
# Returns a unique identifier for this user to use for gitolite keys.
# As login names may change (i.e., user renamed), we use the user id
# with its login name as a prefix for readibility.
def gitolite_identifier
identifier = [RedmineGitHosting::Config.gitolite_identifier_prefix, stripped_login]
identifier.concat(['_', id]) unless RedmineGitHosting::Config.gitolite_identifier_strip_user_id?
identifier.join
end
def gitolite_projects
projects.uniq.select { |p| p.gitolite_repos.any? }
end
# Syntaxic sugar
def status_has_changed?
status_has_changed
end
def allowed_to_manage_repository?(repository)
!roles_for_project(repository.project).select { |role| role.allowed_to?(:manage_repository) }.empty?
end
def allowed_to_commit?(repository)
allowed_to?(:commit_access, repository.project)
end
def allowed_to_clone?(repository)
allowed_to?(:view_changesets, repository.project)
end
def allowed_to_create_ssh_keys?
allowed_to?(:create_gitolite_ssh_key, nil, global: true)
end
def allowed_to_download?(repository)
git_allowed_to?(:download_git_revision, repository)
end
def git_allowed_to?(permission, repository)
if repository.project.active?
allowed_to?(permission, repository.project)
else
allowed_to?(permission, nil, global: true)
end
end
private
# This is Rails method : <attribute>_changed?
# However, the value is cleared before passing the object to the controller.
# We need to save it in virtual attribute to trigger Gitolite resync if changed.
#
def check_if_status_changed
if status_changed?
self.status_has_changed = true
else
self.status_has_changed = false
end
end
def stripped_login
login.underscore.gsub(/[^0-9a-zA-Z]/, '_')
end
end
end
end
unless User.included_modules.include?(RedmineGitHosting::Patches::UserPatch)
User.send(:prepend, RedmineGitHosting::Patches::UserPatch)
end

View file

@ -0,0 +1,74 @@
require_dependency 'users_controller'
module RedmineGitHosting
module Patches
module UsersControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def self.prepended(base)
base.send :helper, GitHostingUsersHelper
base.class_eval do
helper :gitolite_public_keys
helper :bootstrap_kit
end
end
def edit
# Set public key values for view
set_public_key_values
super
end
def update
# Set public key values for view (in case of invalid form)
set_public_key_values
super
# Update projects if needed
update_projects if @user.status_has_changed?
end
def destroy
# Build SSH keys list before user destruction.
ssh_keys_list = ssh_keys_to_destroy
# Destroy user
super
# Destroy SSH keys
destroy_ssh_keys(ssh_keys_list)
end
private
# Add in values for viewing public keys:
def set_public_key_values
@gitolite_user_keys = @user.gitolite_public_keys.user_key.order('title ASC, created_at ASC')
@gitolite_deploy_keys = @user.gitolite_public_keys.deploy_key.order('title ASC, created_at ASC')
end
def update_projects
gitolite_accessor.update_projects(projects_to_update, message: "Status of '#{@user.login}' has changed, update projects")
end
def projects_to_update
@user.gitolite_projects.map(&:id)
end
def ssh_keys_to_destroy
@user.gitolite_public_keys.map(&:data_for_destruction)
end
def destroy_ssh_keys(ssh_keys_list)
RedmineGitHosting.logger.info("User '#{@user.login}' has been deleted from Redmine, delete membership and SSH keys !")
ssh_keys_list.each do |ssh_key|
gitolite_accessor.destroy_ssh_key(ssh_key)
end
end
end
end
end
unless UsersController.included_modules.include?(RedmineGitHosting::Patches::UsersControllerPatch)
UsersController.send(:prepend, RedmineGitHosting::Patches::UsersControllerPatch)
end

View file

@ -0,0 +1,48 @@
require_dependency 'watchers_controller'
module RedmineGitHosting
module Patches
module WatchersControllerPatch
include RedmineGitHosting::GitoliteAccessor::Methods
def create
super
update_repository(@watched)
end
def destroy
super
update_repository(@watched)
end
def watch
super
update_repository(@watchables.first)
end
def unwatch
super
update_repository(@watchables.first)
end
private
def update_repository(repo)
return if !repo.is_a?(Repository::Xitolite)
options = { message: "Watcher changes on repository '#{repo}', update!" }
gitolite_accessor.update_repository(repo, options)
end
end
end
end
unless WatchersController.included_modules.include?(RedmineGitHosting::Patches::WatchersControllerPatch)
WatchersController.send(:prepend, RedmineGitHosting::Patches::WatchersControllerPatch)
end

View file

@ -0,0 +1,53 @@
require_dependency 'watchers_helper'
# This patch fix http://www.redmine.org/issues/12348
module RedmineGitHosting
module Patches
module WatchersHelperPatch
def self.prepended(base)
base.class_eval do
alias_method :watcher_css_without_git_hosting, :watcher_css
alias_method :watcher_css, :watcher_css_with_git_hosting
alias_method :watchers_list_without_git_hosting, :watchers_list
alias_method :watchers_list, :watchers_list_with_git_hosting
end
end
def watcher_css_with_git_hosting(objects, &block)
watcher_css_without_git_hosting(objects, &block).gsub('/', '-')
end
def watchers_list_with_git_hosting(object, &block)
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".gsub('/', '_').to_sym, object.project)
content = ''.html_safe
lis = object.watcher_users.collect do |user|
s = ''.html_safe
s << avatar(user, :size => "16").to_s
s << link_to_user(user, :class => 'user')
if remove_allowed
url = {:controller => 'watchers',
:action => 'destroy',
:object_type => object.class.to_s.underscore,
:object_id => object.id,
:user_id => user}
s << ' '
s << link_to(image_tag('delete.png'), url,
:remote => true, :method => 'delete', :class => "delete")
end
content << content_tag('li', s, :class => "user-#{user.id}")
end
content.present? ? content_tag('ul', content, :class => 'watchers') : content
end
end
end
end
unless WatchersHelper.included_modules.include?(RedmineGitHosting::Patches::WatchersHelperPatch)
WatchersHelper.send(:prepend, RedmineGitHosting::Patches::WatchersHelperPatch)
end

View file

@ -0,0 +1,22 @@
module RedmineGitHosting
class PluginAuthor
attr_reader :author
def initialize(author)
@author = author
end
def name
RedmineGitHosting::Utils::Git.author_name(author)
end
def email
RedmineGitHosting::Utils::Git.author_email(author).downcase
end
end
end

View file

@ -0,0 +1,13 @@
module RedmineGitHosting
module Plugins
extend self
def execute(step, repository, opts = {})
Plugins::GitolitePlugin.all_plugins.each do |plugin|
plugin.new(repository, opts).send(step) if plugin.method_defined?(step)
end
rescue => e
RedmineGitHosting.logger.error e.message
end
end
end

View file

@ -0,0 +1,35 @@
module RedmineGitHosting::Plugins::Extenders
class BaseExtender < RedmineGitHosting::Plugins::GitolitePlugin
attr_reader :repository
attr_reader :recovered
attr_reader :gitolite_repo_name
attr_reader :gitolite_repo_path
attr_reader :git_default_branch
attr_reader :options
def initialize(repository, options = {})
@repository = repository
@recovered = options.delete(:recovered) { false }
@gitolite_repo_name = repository.gitolite_repository_name
@gitolite_repo_path = repository.gitolite_repository_path
@git_default_branch = repository.git_default_branch
@options = options
end
private
def recovered?
recovered
end
def installable?
false
end
end
end

View file

@ -0,0 +1,34 @@
module RedmineGitHosting::Plugins::Extenders
class BranchUpdater < BaseExtender
attr_reader :update_default_branch
def initialize(*args)
super
@update_default_branch = options.delete(:update_default_branch) { false }
end
def post_update
# Update default branch if needed
do_update_default_branch if update_default_branch?
end
private
def update_default_branch?
Additionals.true? update_default_branch
end
def do_update_default_branch
sudo_git('symbolic-ref', 'HEAD', new_default_branch)
rescue RedmineGitHosting::GitHosting::GitHostingException
logger.error("Error while updating default branch for repository '#{gitolite_repo_name}'")
else
logger.info("Default branch successfully updated for repository '#{gitolite_repo_name}'")
repository.empty_cache!
end
def new_default_branch
"refs/heads/#{git_default_branch}"
end
end
end

View file

@ -0,0 +1,33 @@
module RedmineGitHosting::Plugins::Extenders
class ConfigKeyDeletor < BaseExtender
attr_reader :delete_git_config_key
def initialize(*args)
super
@delete_git_config_key = options.delete(:delete_git_config_key) { '' }
end
def post_update
# Delete hook param if needed
delete_hook_param unless delete_git_config_key.nil? || delete_git_config_key.empty?
end
private
def delete_hook_param
begin
sudo_git('config', '--local', '--unset', delete_git_config_key)
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Error while deleting Git config key '#{delete_git_config_key}' for repository '#{gitolite_repo_name}'")
else
logger.info("Git config key '#{delete_git_config_key}' successfully deleted for repository '#{gitolite_repo_name}'")
end
end
end
end

Some files were not shown because too many files have changed in this diff Show more