Añade el plugin Redmine Git Hosting 5.0.0

This commit is contained in:
Manuel Cillero 2021-03-20 13:29:16 +01:00
parent cfa0d58b18
commit a3bddad233
458 changed files with 30396 additions and 1 deletions

View file

@ -0,0 +1,69 @@
---
# 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: ''
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,119 @@
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') unless command
return render_404('Project Not Found') unless @project
self.method(command).call()
end
def post_receive_redmine
@repository = find_repository
return render_404('Repository Not Found') if @repository.nil?
if !valid_encoded_time?(params[:clear_time], params[:encoded_time], @repository.gitolite_hook_key)
return render_403('The hook key provided is not valid. Please let your server admin know about it')
end
@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 unless path_parameters.key?(:type)
case path_parameters[:type]
when 'redmine'
:post_receive_redmine
when 'github'
:post_receive_github
end
end
def find_project
Project.find_by_identifier(path_parameters[:projectid]) if path_parameters.key?(:projectid)
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].present?
@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'.freeze
end

View file

@ -0,0 +1,72 @@
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,531 @@
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 = branches
return nil if bras.nil?
default_bras = bras.select { |x| x.is_default == true }
return default_bras.first.to_s unless 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
name.force_encoding(@path_encoding) if name.respond_to?(:force_encoding)
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)
unless entries.detect { |entry| entry.name == name }
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 })
end
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")
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? || 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(&: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(&: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.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}") unless @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)
ScmData.binary? content
end
end
end
end
end

View file

@ -0,0 +1,69 @@
# 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',
%w[controllers concerns],
%w[models concerns]
def logger
@logger ||= if ['Journald::Logger', 'Journald::TraceLogger'].include? Rails.logger.class.to_s
RedmineGitHosting::JournalLogger.init_logs! logprogname, loglevel
else
RedmineGitHosting::FileLogger.init_logs! logprogname, logfile, loglevel
end
end
def logprogname
'redmine_git_hosting'
end
def logfile
Rails.root.join('log/git_hosting.log')
end
def loglevel
case RedmineGitHosting::Config.gitolite_log_level
when 'debug'
Logger::DEBUG
when 'warn'
Logger::WARN
when 'error'
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,60 @@
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 '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,64 @@
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.to_i.negative? # No expiration needed
current_time = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
current_time - max_cache_time
end
def valid_cache_entry?(cached_entry_date)
return true if max_cache_time.to_i.negative? # 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: repo_id, command: 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,88 @@
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,102 @@
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.to_i.negative? ? 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,27 @@
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,122 @@
module RedmineGitHosting
module Commands
module Git
extend self
############################
# #
# Sudo+Git Shell Wrapper #
# #
############################
# Send Git command with Sudo
#
def sudo_git(*params)
cmd = if RedmineGitHosting::Config.gitolite_use_sudo?
sudo_git_cmd.concat(params)
else
['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)
true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
if code == 5
true
else
logger.error("Error while removing Git global parameter : #{key}")
logger.error(e.output)
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)
true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Error while setting Git global parameter : #{key} (#{value})")
logger.error(e.output)
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
sudo_git('--version')
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Can't retrieve Git version: #{e.output}")
'unknown'
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 unless 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)
true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
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,55 @@
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,203 @@
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)
false
else
begin
sudo_chmod(filemode, dest_file)
true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error(e.output)
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)
code.to_i.zero?
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,59 @@
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,109 @@
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,23 @@
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,92 @@
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
unless 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.key?(:reset) && opts[:reset] == true
unless @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
false
else
if test.match(/#{user}/)
true
else
false
end
end
end
end
end
end

View file

@ -0,0 +1,59 @@
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,79 @@
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,23 @@
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,23 @@
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,21 @@
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,51 @@
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 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,41 @@
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)
to_console message
logger.debug message.strip
end
def info(message)
to_console message
logger.info message.strip
end
def warn
to_console message
logger.warn message.strip
end
def error(message)
to_console message
logger.error message.strip
end
private
def to_console(message)
puts message # rubocop: disable Rails/Output
end
def logger
RedmineGitHosting.logger
end
end
end

View file

@ -0,0 +1,27 @@
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,17 @@
require 'logger'
module RedmineGitHosting
class FileLogger < ::Logger
LOG_LEVELS = %w[debug info warn error].freeze
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,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 = {})
bypass = if options.key?(:bypass_sidekiq) && options[:bypass_sidekiq] == true
true
else
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,39 @@
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,110 @@
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,26 @@
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,186 @@
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,26 @@
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,23 @@
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,41 @@
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,18 @@
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,134 @@
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 unless directory_exists?
return unless install_hook_file
logger.info("Hook '#{name}' installed")
update_gitolite
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}'")
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)
true
rescue RedmineGitHosting::Error::GitoliteCommandException => e
logger.error("Problems installing hook directory '#{parent_path}'")
logger.error(e.output)
false
end
end
end
end

View file

@ -0,0 +1,42 @@
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,21 @@
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,44 @@
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,68 @@
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,99 @@
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!')
else
execute_action(action, object, options)
end
end
private
def flush_cache(options = {})
if options.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,91 @@
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.key?(action)
wrappers[action]
else
raise RedmineGitHosting::Error::GitoliteWrapperException, "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,46 @@
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.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,12 @@
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,38 @@
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 unless 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,46 @@
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
return unless repo_conf.permissions[0]['RW+'][''].include?('DUMMY_REDMINE_KEY')
repo_conf.permissions[0]['RW+'][''].delete('DUMMY_REDMINE_KEY')
end
end
end
end
end

View file

@ -0,0 +1,12 @@
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,35 @@
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,28 @@
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,30 @@
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,44 @@
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,44 @@
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,32 @@
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,29 @@
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,35 @@
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,19 @@
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,19 @@
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,15 @@
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,16 @@
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,76 @@
require 'github/markup'
module RedmineGitHosting
class GitHostingHookListener < Redmine::Hook::ViewListener
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
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))
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
end
def redmine_file?(file)
%w[.txt].include?(File.extname(file.path))
end
def github_file?(file)
%w[.markdown .mdown .mkdn .md].include?(File.extname(file.path))
end
end
end

View file

@ -0,0 +1,38 @@
module RedmineGitHosting
# @see https://github.com/theforeman/journald-logger
if defined? ::Journald::Logger
class JournalLogger < ::Journald::Logger
def self.init_logs!(progname, loglevel)
logger = new progname, type: progname
logger.level = loglevel
logger
end
def debug(msg)
super msg2str(msg)
end
def info(msg)
super msg2str(msg)
end
def warn(msg)
super msg2str(msg)
end
def error(msg)
super msg2str(msg)
end
def msg2str(msg)
case msg
when ::String
msg
else
msg.inspect
end
end
end
end
end

View file

@ -0,0 +1,26 @@
require 'html/pipeline'
require 'task_list/filter'
require 'task_list/railtie'
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,
TaskList::Filter,
HTML::Pipeline::AutolinkFilter,
HTML::Pipeline::TableOfContentsFilter]
end
end
end

View file

@ -0,0 +1,105 @@
module RedmineGitHosting
class MirrorKeysInstaller
attr_reader :gitolite_home_dir, :gitolite_ssh_public_key, :gitolite_ssh_private_key
GITOLITE_MIRRORING_KEYS_NAME = 'redmine_gitolite_admin_id_rsa_mirroring'.freeze
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)
format_mirror_key(File.read(gitolite_ssh_public_key))
rescue => e
RedmineGitHosting.logger.error("Error while loading mirroring public key : #{e.inspect}")
nil
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?
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,60 @@
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.prepend RedmineGitHosting::Patches::ChangesetPatch
end

View file

@ -0,0 +1,28 @@
module RedmineGitHosting
module Patches
module DashboardContentProjectPatch
extend ActiveSupport::Concern
included do
prepend InstanceOverwriteMethods
end
module InstanceOverwriteMethods
def block_definitions
blocks = super
blocks['giturls'] = { label: l(:label_repository_url_plural),
permission: :manage_repository,
no_settings: true,
partial: 'dashboards/blocks/git_urls' }
blocks
end
end
end
end
end
if DashboardContentProject.included_modules.exclude? RedmineGitHosting::Patches::DashboardContentProjectPatch
DashboardContentProject.include RedmineGitHosting::Patches::DashboardContentProjectPatch
end

View file

@ -0,0 +1,127 @@
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)
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.prepend RedmineGitHosting::Patches::GrackAuthPatch
end

View file

@ -0,0 +1,117 @@
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.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.prepend RedmineGitHosting::Patches::GrackServerPatch
end

View file

@ -0,0 +1,31 @@
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, id)
end
end
def user_removed(user)
super
protected_branches.each do |pb|
RepositoryProtectedBranches::MemberManager.new(pb).remove_user_from_group(user, id)
end
end
end
end
end
Group.prepend RedmineGitHosting::Patches::GroupPatch unless Group.included_modules.include?(RedmineGitHosting::Patches::GroupPatch)

View file

@ -0,0 +1,15 @@
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
Issue.prepend RedmineGitHosting::Patches::IssuePatch unless Issue.included_modules.include?(RedmineGitHosting::Patches::IssuePatch)

View file

@ -0,0 +1,15 @@
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
Journal.prepend RedmineGitHosting::Patches::JournalPatch unless Journal.included_modules.include?(RedmineGitHosting::Patches::JournalPatch)

View file

@ -0,0 +1,24 @@
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
Member.prepend RedmineGitHosting::Patches::MemberPatch unless Member.included_modules.include?(RedmineGitHosting::Patches::MemberPatch)

View file

@ -0,0 +1,53 @@
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.present?
# 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
Project.prepend RedmineGitHosting::Patches::ProjectPatch unless Project.included_modules.include?(RedmineGitHosting::Patches::ProjectPatch)

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 :git_hosting
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 :git_hosting
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,17 @@
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.prepend RedmineGitHosting::Patches::RepositoriesHelperPatch
end

View file

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

View file

@ -0,0 +1,40 @@
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.prepend RedmineGitHosting::Patches::RolesControllerPatch
end

View file

@ -0,0 +1,25 @@
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
Setting.prepend RedmineGitHosting::Patches::SettingPatch unless Setting.included_modules.include?(RedmineGitHosting::Patches::SettingPatch)

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 :git_hosting
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.prepend RedmineGitHosting::Patches::SettingsControllerPatch
end

View file

@ -0,0 +1,23 @@
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.prepend RedmineGitHosting::Patches::SysControllerPatch
end

View file

@ -0,0 +1,89 @@
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
self.status_has_changed = if status_changed?
true
else
false
end
end
def stripped_login
login.underscore.gsub(/[^0-9a-zA-Z]/, '_')
end
end
end
end
User.prepend RedmineGitHosting::Patches::UserPatch unless User.included_modules.include?(RedmineGitHosting::Patches::UserPatch)

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.class_eval do
helper :gitolite_public_keys
helper :git_hosting
helper :git_hosting_users
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, :created_at)
@gitolite_deploy_keys = @user.gitolite_public_keys.deploy_key.order(:title, :created_at)
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.prepend RedmineGitHosting::Patches::UsersControllerPatch
end

View file

@ -0,0 +1,42 @@
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 unless 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.prepend RedmineGitHosting::Patches::WatchersControllerPatch
end

View file

@ -0,0 +1,51 @@
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)
watcher_css_without_git_hosting(objects).tr '/', '-'
end
def watchers_list_with_git_hosting(object)
remove_allowed = User.current.allowed_to? "delete_#{object.class.name.underscore}_watchers".tr('/', '_').to_sym, object.project
content = ''.html_safe
object.watcher_users.preload(:email_address).each 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(l(:button_delete), url,
remote: true, method: 'delete',
class: 'delete icon-only icon-del',
title: l(:button_delete))
end
content << tag.li(s, class: "user-#{user.id}")
end
content.present? ? tag.ul(content, class: 'watchers') : content
end
end
end
end
unless WatchersHelper.included_modules.include?(RedmineGitHosting::Patches::WatchersHelperPatch)
WatchersHelper.prepend RedmineGitHosting::Patches::WatchersHelperPatch
end

View file

@ -0,0 +1,17 @@
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,24 @@
module RedmineGitHosting::Plugins::Extenders
class BaseExtender < RedmineGitHosting::Plugins::GitolitePlugin
attr_reader :repository, :recovered, :gitolite_repo_name, :gitolite_repo_path, :git_default_branch, :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

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