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,13 @@
module Gitolitable
extend ActiveSupport::Concern
include Gitolitable::Authorizations
include Gitolitable::Cache
include Gitolitable::Config
include Gitolitable::Features
include Gitolitable::Notifications
include Gitolitable::Paths
include Gitolitable::Permissions
include Gitolitable::Urls
include Gitolitable::Users
include Gitolitable::Validations
end

View file

@ -0,0 +1,73 @@
module Gitolitable
module Authorizations
extend ActiveSupport::Concern
# These are for repository Gitolite configuration
def git_daemon_available?
User.anonymous.allowed_to?(:view_changesets, project) && git_daemon_enabled?
end
def git_web_available?
User.anonymous.allowed_to?(:browse_repository, project) && smart_http_enabled?
end
def protected_branches_available?
protected_branches_enabled? && project.active? && protected_branches.any?
end
def clonable_via_http?
User.anonymous.allowed_to?(:view_changesets, project) || smart_http_enabled?
end
def pushable_via_http?
https_access_enabled?
end
def git_notification_available?
git_notification_enabled? && !mailing_list.empty?
end
# These are for repository URLs
def urls_are_viewable?
User.current.allowed_to?(:view_changesets, project)
end
def ssh_access_available?
git_ssh_enabled? && !git_annex_enabled? && User.current.allowed_to_commit?(self)
end
def https_access_available?
https_access_enabled?
end
def http_access_available?
http_access_enabled?
end
def git_access_available?
(public_project? || public_repo?) && git_daemon_enabled?
end
def go_access_available?
(public_project? || public_repo?) && smart_http_enabled? && git_go_enabled?
end
def git_annex_access_available?
git_annex_enabled?
end
def downloadable?
git_annex_enabled? ? false : User.current.allowed_to_download?(self)
end
def deletable?
RedmineGitHosting::Config.delete_git_repositories?
end
def movable?
identifier.present?
end
end
end

View file

@ -0,0 +1,83 @@
module Gitolitable
module Cache
extend ActiveSupport::Concern
included do
class << self
# Are repositories identifier unique?
#
def repo_ident_unique?
RedmineGitHosting::Config.unique_repo_identifier?
end
# Translate repository path into a unique ID for use in caching of git commands.
#
def repo_path_to_git_cache_id(repo_path)
repo = find_by_path(repo_path, loose: true)
repo ? repo.git_cache_id : nil
end
# Parse a path of the form <proj1>/<proj2>/<proj3>/<repo> and return the specified
# repository. If either 'repo_ident_unique?' is true or the <repo> is a project
# identifier, just return the last component. Otherwise,
# use the immediate parent (<proj3>) to try to identify the repo.
#
# Flags:
# :loose => true : Try to identify corresponding repo even if path is not quite correct
#
# Note that the :loose flag is used when interpreting the contents of the
# repository. If switching back and forth between the "repo_ident_unique?"
# form, it will still identify the repository (as long as there are not more than
# one repo with the same identifier.
#
# Example of data captured by regex :
# <MatchData "test/test2/test3/test4/test5.git" 1:"test4/" 2:"test4" 3:"test5" 4:".git">
# <MatchData "blabla2.git" 1:nil 2:nil 3:"blabla2" 4:".git">
#
def find_by_path(path, flags = {})
parseit = path.match(/\A.*?(([^\/]+)\/)?([^\/]+?)(\.git)?\z/)
return nil if parseit.nil?
project = Project.find_by_identifier(parseit[3])
# return default or first repo with blank identifier (or first Git repo--very rare?)
if project
project.repository || project.repo_blank_ident || project.gitolite_repos.first
elsif repo_ident_unique? || flags[:loose] && parseit[2].nil?
find_by_identifier(parseit[3])
elsif parseit[2]
project = Project.find_by_identifier(parseit[2])
if project.nil?
find_by_identifier(parseit[3])
else
find_by_identifier_and_project_id(parseit[3], project.id) || (flags[:loose] && find_by_identifier(parseit[3]))
end
end
end
end
end
# If repositories identifiers are unique, identifier forms a unique label,
# else use directory notation: <project identifier>/<repo identifier>
#
def git_cache_id
if identifier.blank?
# Should only happen with one repo/project (the default)
project.identifier
elsif self.class.repo_ident_unique?
identifier
else
"#{project.identifier}/#{identifier}"
end
end
# Note: RedmineGitHosting::Cache doesn't know about repository object, it only knows *git_cache_id*.
#
def empty_cache!
RedmineGitHosting::Cache.clear_cache_for_repository(git_cache_id)
end
end
end

View file

@ -0,0 +1,78 @@
module Gitolitable
module Config
extend ActiveSupport::Concern
def git_config
repo_conf = {}
# This is needed for all Redmine repositories
repo_conf['redminegitolite.projectid'] = project.identifier.to_s
repo_conf['redminegitolite.repositoryid'] = identifier || ''
repo_conf['redminegitolite.repositorykey'] = gitolite_hook_key
if project.active?
repo_conf['http.uploadpack'] = clonable_via_http?.to_s
repo_conf['http.receivepack'] = pushable_via_http?.to_s
if git_notification_available?
repo_conf['multimailhook.enabled'] = 'true'
repo_conf['multimailhook.mailinglist'] = mailing_list.join(', ')
repo_conf['multimailhook.from'] = sender_address
repo_conf['multimailhook.emailPrefix'] = email_prefix
else
repo_conf['multimailhook.enabled'] = 'false'
end
git_config_keys.each do |git|
repo_conf[git.key] = git.value
end if git_config_keys.any?
else
# Disable repository
repo_conf['http.uploadpack'] = 'false'
repo_conf['http.receivepack'] = 'false'
repo_conf['multimailhook.enabled'] = 'false'
end
repo_conf
end
def gitolite_options
repo_conf = {}
git_option_keys.each do |option|
repo_conf[option.key] = option.value
end if git_option_keys.any?
repo_conf
end
def owner
{ name: Setting['app_title'], email: Setting['mail_from'] }
end
def github_payload
{
repository: { owner: owner,
description: project.description,
fork: false,
forks: 0,
homepage: project.homepage,
name: redmine_name,
open_issues: project.issues.open.length,
watchers: 0,
private: !project.is_public,
url: repository_url },
pusher: owner
}
end
def repository_url
Rails.application.routes.url_helpers.url_for(
controller: 'repositories', action: 'show',
id: project, repository_id: identifier_param,
only_path: false, host: Setting['host_name'], protocol: Setting['protocol']
)
end
end
end

View file

@ -0,0 +1,79 @@
module Gitolitable
module Features
extend ActiveSupport::Concern
# Always true to force repository fetch_changesets.
def report_last_commit
true
end
# Always true to force repository fetch_changesets.
def extra_report_last_commit
true
end
def git_default_branch
extra[:default_branch]
end
def gitolite_hook_key
extra[:key]
end
def git_daemon_enabled?
extra[:git_daemon]
end
def git_annex_enabled?
extra[:git_annex]
end
def git_notification_enabled?
extra[:git_notify]
end
def git_ssh_enabled?
extra[:git_ssh]
end
def git_go_enabled?
extra[:git_go]
end
def https_access_enabled?
extra[:git_https]
end
def http_access_enabled?
extra[:git_http]
end
def smart_http_enabled?
https_access_enabled? || http_access_enabled?
end
def only_https_access_enabled?
https_access_enabled? && !http_access_enabled?
end
def only_http_access_enabled?
http_access_enabled? && !https_access_enabled?
end
def protected_branches_enabled?
extra[:protected_branch]
end
def public_project?
project.is_public?
end
def public_repo?
extra[:public_repo]
end
def urls_order
extra[:urls_order]
end
end
end

View file

@ -0,0 +1,29 @@
module Gitolitable
module Notifications
extend ActiveSupport::Concern
def mailing_list
default_list + global_include_list - global_exclude_list
end
def default_list
watcher_users.map(&:email_address).map(&:address)
end
def global_include_list
RedmineGitHosting::Config.gitolite_notify_global_include
end
def global_exclude_list
RedmineGitHosting::Config.gitolite_notify_global_exclude
end
def sender_address
extra.notification_sender.presence || RedmineGitHosting::Config.gitolite_notify_global_sender_address
end
def email_prefix
extra.notification_prefix.presence || RedmineGitHosting::Config.gitolite_notify_global_prefix
end
end
end

View file

@ -0,0 +1,90 @@
module Gitolitable
module Paths
extend ActiveSupport::Concern
# This is the repository path from Redmine point of view.
# It is used to build HTTP(s) urls (including GoLang url).
# It doesn't contain references to internal directories like *gitolite_global_storage_dir* or *gitolite_redmine_storage_dir*
# to stay abstract from the real repository location.
# In this case, the real repository path is deduced from the path given thanks to the *find_by_path* method.
#
# Example : blabla/test-blabla/uuuuuuuuuuu/oooooo
#
# Call File.expand_path to add then remove heading /
#
def redmine_repository_path
File.expand_path(File.join('./',
get_full_parent_path,
git_cache_id), '/')[1..-1]
end
# This is the Gitolite repository identifier as it should appear in Gitolite config file.
# Example : redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo
# (with 'redmine' a subdir of the Gitolite storage directory)
#
# Call File.expand_path to add then remove heading /
#
def gitolite_repository_name
File.expand_path(File.join('./',
RedmineGitHosting::Config.gitolite_redmine_storage_dir,
get_full_parent_path,
git_cache_id), '/')[1..-1]
end
# The Gitolite repository identifier with the .git extension.
# Example : redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo.git
#
def gitolite_repository_name_with_extension
"#{gitolite_repository_name}.git"
end
# This is the relative path to the Gitolite repository.
# Example : repositories/redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo.git
# (with 'repositories' the Gitolite storage directory).
#
def gitolite_repository_path
File.join(RedmineGitHosting::Config.gitolite_global_storage_dir, gitolite_repository_name_with_extension)
end
# This is the full absolute path to the Gitolite repository.
# Example : /home/git/repositories/redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo.git
#
def gitolite_full_repository_path
File.join(RedmineGitHosting::Config.gitolite_home_dir, gitolite_repository_path)
end
# A syntaxic sugar used to move repository from a location to an other
# Example : repositories/blabla/test-blabla/uuuuuuuuuuu/oooooo
#
def new_repository_name
gitolite_repository_name
end
# Used to move repository from a location to an other.
# At this point repository url still points to the old location but
# it contains the Gitolite storage directory in its path and the '.git' extension.
# Strip them to get the old repository name.
# Example :
# before : repositories/redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo.git
# after : redmine/blabla/test-blabla/uuuuuuuuuuu/oooooo
#
def old_repository_name
url.gsub(RedmineGitHosting::Config.gitolite_global_storage_dir, '').gsub('.git', '')
end
private
def get_full_parent_path
return '' unless RedmineGitHosting::Config.hierarchical_organisation?
parent_parts = []
p = project
while p.parent
parent_id = p.parent.identifier.to_s
parent_parts.unshift(parent_id)
p = p.parent
end
parent_parts.join('/')
end
end
end

View file

@ -0,0 +1,60 @@
module Gitolitable
module Permissions
extend ActiveSupport::Concern
def build_gitolite_permissions(old_perms = {})
permissions_builder.build(self, gitolite_users, old_perms)
end
# We assume here that ':gitolite_config_file' is different than 'gitolite.conf'
# like 'redmine.conf' with 'include "redmine.conf"' in 'gitolite.conf'.
# This way, we know that all repos in this file are managed by Redmine so we
# don't need to backup users
#
def backup_gitolite_permissions(current_permissions)
if protected_branches_available? || RedmineGitHosting::Config.gitolite_identifier_prefix == ''
{}
else
extract_permissions(current_permissions)
end
end
private
def permissions_builder
if protected_branches_available?
PermissionsBuilder::ProtectedBranches
else
PermissionsBuilder::Standard
end
end
SKIP_USERS = %w[gitweb daemon DUMMY_REDMINE_KEY REDMINE_ARCHIVED_PROJECT REDMINE_CLOSED_PROJECT].freeze
def extract_permissions(current_permissions)
old_permissions = {}
current_permissions.each do |perm, branch_settings|
old_permissions[perm] = {}
branch_settings.each do |branch, user_list|
next if user_list.empty?
new_user_list = []
user_list.each do |user|
# ignore these users
next if SKIP_USERS.include?(user)
# backup users that are not Redmine users
new_user_list.push(user) unless user.include?(RedmineGitHosting::Config.gitolite_identifier_prefix)
end
old_permissions[perm][branch] = new_user_list if new_user_list.any?
end
end
old_permissions
end
end
end

View file

@ -0,0 +1,114 @@
module Gitolitable
module Urls
extend ActiveSupport::Concern
def http_user_login
User.current.anonymous? ? '' : "#{User.current.login}@"
end
def git_access_path
gitolite_repository_name_with_extension
end
def http_access_path
"#{RedmineGitHosting::Config.http_server_subdir}#{redmine_repository_path}.git"
end
def go_access_path
"go/#{redmine_repository_path}"
end
def ssh_url
url = "ssh://#{RedmineGitHosting::Config.gitolite_user}@#{RedmineGitHosting::Config.ssh_server_domain}"
url << if RedmineGitHosting::Config.gitolite_server_port == '22'
"/#{git_access_path}"
else
":#{RedmineGitHosting::Config.gitolite_server_port}/#{git_access_path}"
end
url
end
def git_url
"git://#{RedmineGitHosting::Config.ssh_server_domain}/#{git_access_path}"
end
def http_url
"http://#{http_user_login}#{RedmineGitHosting::Config.http_root_url}/#{http_access_path}"
end
def https_url
"https://#{http_user_login}#{RedmineGitHosting::Config.https_root_url}/#{http_access_path}"
end
def git_annex_url
"#{RedmineGitHosting::Config.gitolite_user}@#{RedmineGitHosting::Config.ssh_server_domain}:#{git_access_path}"
end
# This is the url used by Go to clone repository
#
def go_access_url
return '' unless smart_http_enabled?
return https_url if https_access_available?
return http_url if http_access_available?
end
# This is the url to add in Go files
#
def go_url
return '' unless smart_http_enabled?
return "#{RedmineGitHosting::Config.https_root_url}/#{go_access_path}" if https_access_available?
return "#{RedmineGitHosting::Config.http_root_url}/#{go_access_path}" if http_access_available?
end
def ssh_access
{ url: ssh_url, committer: User.current.allowed_to_commit?(self).to_s }
end
## Unsecure channels (clear password), commit is disabled
def http_access
{ url: http_url, committer: 'false' }
end
def https_access
{ url: https_url, committer: User.current.allowed_to_commit?(self).to_s }
end
def git_access
{ url: git_url, committer: 'false' }
end
def git_annex_access
{ url: git_annex_url, committer: User.current.allowed_to_commit?(self).to_s }
end
def go_access
{ url: go_url, committer: 'false' }
end
def available_urls
hash = {}
hash[:ssh] = ssh_access if ssh_access_available?
hash[:https] = https_access if https_access_available?
hash[:http] = http_access if http_access_available?
hash[:git] = git_access if git_access_available?
hash[:go] = go_access if go_access_available?
hash[:git_annex] = git_annex_access if git_annex_access_available?
hash
end
def available_urls_sorted
return available_urls if urls_order.blank?
hash = {}
urls_order.each do |url|
available_url = available_urls[url.to_sym]
next if available_url.blank?
hash[url.to_sym] = available_url
end
hash
end
end
end

View file

@ -0,0 +1,95 @@
module Gitolitable
module Users
extend ActiveSupport::Concern
def gitolite_users
if project.active?
users_for_active_project
elsif project.archived?
users_for_archived_project
else
users_for_closed_project
end
end
def users_for_active_project
data = {}
data[:rewind_users] = rewind_users + rewind_deploy_users
data[:write_users] = write_users
data[:read_users] = read_users + read_deploy_users
data[:developer_team] = developer_team
data[:all_read] = all_users
# Add other users
data[:read_users] << 'DUMMY_REDMINE_KEY' if read_users.empty? && write_users.empty? && rewind_users.empty?
data[:read_users] << 'gitweb' if git_web_available?
data[:read_users] << 'daemon' if git_daemon_available?
# Return users
data
end
def users_for_archived_project
data = {}
data[:read_users] = ['REDMINE_ARCHIVED_PROJECT']
data
end
def users_for_closed_project
data = {}
data[:read_users] = all_users
data[:read_users] << 'REDMINE_CLOSED_PROJECT'
data
end
def users
project.users_available
end
def rewind_users
@rewind_users ||= users.select { |u| u.allowed_to?(:manage_repository, project) }.map { |u| u.gitolite_identifier }.sort
end
def write_users
@write_users ||= users.select { |u| u.allowed_to?(:commit_access, project) }.map { |u| u.gitolite_identifier }.sort - rewind_users
end
def read_users
@read_users ||= users.select { |u| u.allowed_to?(:view_changesets, project) }
.map { |u| u.gitolite_identifier }
.sort - rewind_users - write_users
end
def developer_team
@developer_team ||= (rewind_users + write_users).sort
end
def all_users
@all_users ||= (rewind_users + write_users + read_users).sort
end
def rewind_deploy_users
deploy_users_for_keys rewind_deploy_keys
end
def read_deploy_users
deploy_users_for_keys read_deploy_keys
end
def rewind_deploy_keys
deploy_keys_by_perm 'RW+'
end
def read_deploy_keys
deploy_keys_by_perm 'R'
end
def deploy_keys_by_perm(perm)
deployment_credentials.active.select { |cred| cred.perm == perm }
end
def deploy_users_for_keys(keys)
keys.map { |cred| cred.gitolite_public_key.owner }
end
end
end

View file

@ -0,0 +1,113 @@
module Gitolitable
module Validations
extend ActiveSupport::Concern
included do
# Set URL ourself as relative path.
#
before_validation :set_git_urls
# 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
validate :identifier_dont_change
validate :default_repository_has_identifier
class << self
# Build a hash of repository identifier :
# <repo_1_identifier> => 1
# <repo_2_identifier> => 1
# etc...
# If the same repository identifier is found many times, increment the corresponding counter.
# Repository identifiers are unique if all values of the hash are 1.
#
def identifiers_to_hash
all.map(&:identifier).inject(Hash.new(0)) do |h, x|
h[x] += 1 if x.present?
h
end
end
def have_duplicated_identifier?
(identifiers_to_hash.values.max || 0) > 1
end
end
end
def exists_in_gitolite?
RedmineGitHosting::Commands.sudo_dir_exists?(gitolite_repository_path)
end
def empty_in_gitolite?
RedmineGitHosting::Commands.sudo_repository_empty?(gitolite_repository_path)
end
def git_objects_count
RedmineGitHosting::Commands.sudo_git_objects_count(File.join(gitolite_repository_path, 'objects'))
end
def empty?
extra_info.nil? || (!extra_info.key?('heads') && !extra_info.key?('branches'))
end
def data_for_destruction
{
repo_name: gitolite_repository_name,
repo_path: gitolite_full_repository_path,
delete_repository: deletable?,
git_cache_id: git_cache_id
}
end
private
# Set up git urls for new repositories
#
def set_git_urls
self.url = gitolite_repository_path if url.blank?
self.root_url = url if root_url.blank?
end
# Check several aspects of repository identifier (only for Redmine 1.4+)
# 1) cannot equal identifier of any project
# 2) if repo_ident_unique? make sure that repo identifier is globally unique
# 3) cannot make this repo the default if there will be some other repo with blank identifier
#
def additional_constraints_on_identifier
if identifier.present? && (new_record? || identifier_changed?)
errors.add(:identifier, :cannot_equal_project) if Project.find_by_identifier(identifier)
# See if a repo for another project has the same identifier (existing validations already check for current project)
if self.class.repo_ident_unique? && Repository.where("identifier = ? and project_id <> ?", identifier, project.id).any?
errors.add :identifier, :taken
end
end
end
# Make sure identifier hasn't changed. Allow null and blank
# Note that simply using identifier_changed doesn't seem to work
# if the identifier was "NULL" but the new identifier is ""
#
def identifier_dont_change
return if new_record?
if (identifier_was.blank? && identifier.present?) || (identifier_was.present? && identifier_changed?)
errors.add :identifier, :cannot_change
end
end
# Need to make sure that we don't take the default slot away from a sibling repo with blank identifier
#
def default_repository_has_identifier
if project && (is_default? || set_as_default?)
possibles = Repository.where("project_id = ? and (identifier = '' or identifier is null)", project.id)
errors.add(:base, :blank_default_exists) if possibles.any? && (new_record? || possibles.detect { |x| x.id != id })
end
end
end
end

View file

@ -0,0 +1,21 @@
class GitCache < ActiveRecord::Base
include Redmine::SafeAttributes
CACHE_ADAPTERS = [%w[Database database],
%w[Memcached memcached],
%w[Redis redis]].freeze
## Attributes
safe_attributes 'repo_identifier', 'command', 'command_output'
## Validations
validates :repo_identifier, presence: true
validates :command, presence: true
validates :command_output, presence: true
class << self
def adapters
CACHE_ADAPTERS.map(&:last)
end
end
end

View file

@ -0,0 +1,8 @@
class GithubComment < ActiveRecord::Base
## Relations
belongs_to :journal
## Validations
validates :github_id, presence: true
validates :journal_id, presence: true, uniqueness: { scope: :github_id }
end

View file

@ -0,0 +1,8 @@
class GithubIssue < ActiveRecord::Base
## Relations
belongs_to :issue
## Validations
validates :github_id, presence: true
validates :issue_id, presence: true, uniqueness: { scope: :github_id }
end

View file

@ -0,0 +1,203 @@
class GitolitePublicKey < ActiveRecord::Base
include Redmine::SafeAttributes
TITLE_LENGTH_LIMIT = 60
KEY_TYPE_USER = 0
KEY_TYPE_DEPLOY = 1
## Attributes
safe_attributes 'title', 'key', 'key_type', 'delete_when_unused'
## Relations
belongs_to :user
has_many :repository_deployment_credentials, dependent: :destroy
## Validations
validates :user_id, presence: true
validates :title, presence: true, uniqueness: { case_sensitive: false, scope: :user_id },
length: { maximum: TITLE_LENGTH_LIMIT }, format: /\A[a-z0-9_\-]*\z/i
validates :identifier, presence: true, uniqueness: { case_sensitive: false, scope: :user_id }
validates :key, presence: true
validates :key_type, presence: true, numericality: { only_integer: true },
inclusion: { in: [KEY_TYPE_USER, KEY_TYPE_DEPLOY] }
validate :has_not_been_changed
validate :key_correctness
validate :key_not_admin
validate :key_uniqueness
## Scopes
scope :user_key, -> { where(key_type: KEY_TYPE_USER) }
scope :deploy_key, -> { where(key_type: KEY_TYPE_DEPLOY) }
scope :sorted, -> { order(:title, :created_at) }
## Callbacks
before_validation :strip_whitespace
before_validation :remove_control_characters
before_validation :set_identifier
before_validation :set_fingerprint
def key_type_as_string
user_key? ? 'user_key' : 'deploy_key'
end
def to_s
title
end
def data_for_destruction
{ title: identifier, key: key, location: location, owner: owner }
end
# Returns the path to this key under the gitolite keydir
# resolves to <user.gitolite_identifier>/<location>/<owner>.pub
#
# tile: test-key
# identifier: redmine_admin_1@redmine_test_key
# identifier: redmine_admin_1@redmine_deploy_key_1
#
#
# keydir/
# ├── redmine_git_hosting
# │   └── redmine_admin_1
# │   ├── redmine_test_key
# │   │   └── redmine_admin_1.pub
# │   ├── redmine_deploy_key_1
# │   │   └── redmine_admin_1.pub
# │   └── redmine_deploy_key_2
# │   └── redmine_admin_1.pub
# └── redmine_gitolite_admin_id_rsa.pub
#
#
# The root folder for this user is the user's identifier
# for logical grouping of their keys, which are organized
# by their title in subfolders.
#
# This is due to the new gitolite multi-keys organization
# using folders. See https://gitolite.com/gitolite/users.html
def gitolite_path
File.join('keydir', RedmineGitHosting::Config.gitolite_key_subdir, user.gitolite_identifier, location, owner) + '.pub'
end
# Make sure that current identifier is consistent with current user login.
# This method explicitly overrides the static nature of the identifier
def reset_identifiers(opts = {})
# Fix identifier
self.identifier = nil
self.fingerprint = nil
self.identifier = GitolitePublicKeys::GenerateIdentifier.call(self, user, opts)
set_fingerprint
# Need to override the "never change identifier" constraint
save(validate: false)
end
# Key type checking functions
def user_key?
key_type == KEY_TYPE_USER
end
def deploy_key?
key_type == KEY_TYPE_DEPLOY
end
def owner
identifier.split('@')[0]
end
def location
identifier.split('@')[1]
end
def type
key.split(' ')[0]
end
def blob
key.split(' ')[1]
end
def email
key.split(' ')[2]
end
private
# Strip leading and trailing whitespace
# Don't mess with existing keys (since cannot change key text anyway)
#
def strip_whitespace
return unless new_record?
self.title = title.strip rescue ''
self.key = key.strip rescue ''
end
# Remove control characters from key
# Don't mess with existing keys (since cannot change key text anyway)
#
def remove_control_characters
return unless new_record?
self.key = RedmineGitHosting::Utils::Ssh.sanitize_ssh_key(key)
end
# Returns the unique identifier for this key based on the key_type
#
# For user public keys, this simply is the user's gitolite_identifier.
# For deployment keys, we use an incrementing number.
#
def set_identifier
return nil if user_id.nil?
self.identifier ||= GitolitePublicKeys::GenerateIdentifier.call(self, user)
end
def set_fingerprint
self.fingerprint = RedmineGitHosting::Utils::Ssh.ssh_fingerprint(key)
rescue RedmineGitHosting::Error::InvalidSshKey => e
errors.add(:key, :corrupted)
end
def has_not_been_changed
return if new_record?
%w[identifier key user_id key_type title fingerprint].each do |attribute|
method = "#{attribute}_changed?"
errors.add(attribute, :cannot_change) if send(method)
end
end
# Test correctness of fingerprint from output
# and general ssh-(r|d|ecd)sa <key> <id> structure
#
def key_correctness
return false if key.nil?
key.match(/^(\S+)\s+(\S+)/) && (fingerprint =~ /^(\w{2}:?)+$/i)
end
def key_not_admin
errors.add(:key, :taken_by_gitolite_admin) if fingerprint == RedmineGitHosting::Config.gitolite_ssh_public_key_fingerprint
end
def key_uniqueness
return unless new_record?
existing = GitolitePublicKey.find_by_fingerprint(fingerprint)
return unless existing
if existing.user == User.current
errors.add(:key, :taken_by_you, name: existing.title)
elsif User.current.admin?
errors.add(:key, :taken_by_other, login: existing.user.login, name: existing.title)
else
errors.add(:key, :taken_by_someone)
end
end
end

View file

@ -0,0 +1,24 @@
class ProtectedBranchesMember < ActiveRecord::Base
include Redmine::SafeAttributes
## Attributes
safe_attributes 'principal_id', 'inherited_by'
## Relations
belongs_to :protected_branch, class_name: 'RepositoryProtectedBranche'
belongs_to :principal
## Callbacks
after_destroy :remove_dependent_objects
private
def remove_dependent_objects
return unless principal.class.name == 'Group'
principal.users.each do |user|
member = self.class.find_by_principal_id_and_inherited_by(user.id, principal.id)
member&.destroy!
end
end
end

View file

@ -0,0 +1,74 @@
require_dependency 'redmine/scm/adapters/xitolite_adapter'
class Repository::Xitolite < Repository::Git
# Include Gitolitable concern
include Gitolitable
# Virtual attributes
attr_accessor :create_readme
attr_accessor :enable_git_annex
# Redmine uses safe_attributes on Repository, so we need to declare our virtual attributes.
safe_attributes 'create_readme', 'enable_git_annex'
# Relations
has_one :extra, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryGitExtra'
has_many :mirrors, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryMirror'
has_many :post_receive_urls, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryPostReceiveUrl'
has_many :deployment_credentials, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryDeploymentCredential'
has_many :git_keys, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryGitConfigKey'
has_many :git_config_keys, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryGitConfigKey::GitConfig'
has_many :git_option_keys, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryGitConfigKey::Option'
has_many :protected_branches, dependent: :destroy, foreign_key: 'repository_id', class_name: 'RepositoryProtectedBranche'
# Additionnal validations
validate :valid_repository_options, on: :create
acts_as_watchable
class << self
def scm_adapter_class
Redmine::Scm::Adapters::XitoliteAdapter
end
def scm_name
'Gitolite'
end
end
def sti_name
'Repository::Xitolite'
end
# Override the original method to accept options hash
# which may contain *bypass_cache* flag.
#
def diff(path, rev, rev_to, opts = {})
scm.diff(path, rev, rev_to, opts)
end
def rev_list(revision, args = [])
scm.rev_list(revision, args)
end
def rev_parse(revision)
scm.rev_parse(revision)
end
def archive(revision, format = 'tar')
scm.archive(revision, format)
end
def mirror_push(url, branch, args = [])
scm.mirror_push(url, branch, args)
end
private
def valid_repository_options
return unless Additionals.true? create_readme
return unless Additionals.true? enable_git_annex
errors.add(:base, :invalid_options)
end
end

View file

@ -0,0 +1,56 @@
class RepositoryDeploymentCredential < ActiveRecord::Base
include Redmine::SafeAttributes
VALID_PERMS = ['R', 'RW+'].freeze
DEFAULT_PERM = 'RW+'.freeze
## Attributes
safe_attributes 'perm', 'active', 'gitolite_public_key_id'
## Relations
belongs_to :repository
belongs_to :gitolite_public_key
belongs_to :user
## Validations
validates :repository_id, presence: true,
uniqueness: { scope: :gitolite_public_key_id }
validates :gitolite_public_key_id, presence: true, exclusion: { in: [-1] }
validates :user_id, presence: true
validates :perm, presence: true,
inclusion: { in: VALID_PERMS }
validates_associated :repository
validates_associated :gitolite_public_key
validates_associated :user
validate :correct_key_type
validate :owner_matches_key
## Scopes
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :sorted, -> { order(:id) }
def to_s
"#{repository.identifier}-#{gitolite_public_key.identifier} : #{perm}"
end
# Deployment Credentials ignored unless created by someone who still has permission to create them
def honored?
user.admin? || user.allowed_to?(:create_repository_deployment_credentials, repository.project)
end
private
def correct_key_type
errors.add(:base, :invalid_key) if gitolite_public_key && gitolite_public_key.key_type_as_string != 'deploy_key'
end
def owner_matches_key
return if user.nil? || gitolite_public_key.nil?
errors.add(:base, :invalid_user) if user != gitolite_public_key.user
end
end

View file

@ -0,0 +1,42 @@
class RepositoryGitConfigKey < ActiveRecord::Base
include Redmine::SafeAttributes
## Attributes
safe_attributes 'type', 'key', 'value'
## Relations
belongs_to :repository
## Validations
validates :repository_id, presence: true
validates :type, presence: true, inclusion: { in: ['RepositoryGitConfigKey::GitConfig', 'RepositoryGitConfigKey::Option'] }
validates :value, presence: true
## Callbacks
after_save :check_if_key_changed
## Virtual attribute
attr_accessor :key_has_changed
attr_accessor :old_key
# Syntaxic sugar
def key_has_changed?
key_has_changed
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_key_changed
if key_changed?
self.key_has_changed = true
self.old_key = key_change[0]
else
self.key_has_changed = false
self.old_key = ''
end
end
end

View file

@ -0,0 +1,7 @@
class RepositoryGitConfigKey::GitConfig < RepositoryGitConfigKey
VALID_CONFIG_KEY_REGEX = /\A[a-zA-Z0-9]+\.[a-zA-Z0-9.]+\z/
validates :key, presence: true,
uniqueness: { case_sensitive: false, scope: %i[type repository_id] },
format: { with: VALID_CONFIG_KEY_REGEX }
end

View file

@ -0,0 +1,4 @@
class RepositoryGitConfigKey::Option < RepositoryGitConfigKey
validates :key, presence: true,
uniqueness: { case_sensitive: false, scope: %i[type repository_id] }
end

View file

@ -0,0 +1,115 @@
class RepositoryGitExtra < ActiveRecord::Base
include Redmine::SafeAttributes
SMART_HTTP_OPTIONS = [[l(:label_disabled), '0'],
[l(:label_http_only), '3'],
[l(:label_https_only), '1'],
[l(:label_https_and_http), '2']].freeze
ALLOWED_URLS = %w[ssh http https go git git_annex].freeze
URLS_ICONS = { go: { label: 'Go', icon: 'fab_google' },
http: { label: 'HTTP', icon: 'fas_external-link-alt' },
https: { label: 'HTTPS', icon: 'fas_external-link-alt' },
ssh: { label: 'SSH', icon: 'fas_shield-alt' },
git: { label: 'Git', icon: 'fab_git' },
git_annex: { label: 'GitAnnex', icon: 'fab_git' } }.freeze
## Attributes
safe_attributes 'git_http', 'git_https', 'git_ssh', 'git_go', 'git_daemon', 'git_notify', 'git_annex', 'default_branch',
'protected_branch', 'public_repo', 'key', 'urls_order', 'notification_sender', 'notification_prefix'
## Relations
belongs_to :repository
## Validations
validates :repository_id, presence: true, uniqueness: true
validates :default_branch, presence: true
validates :key, presence: true
validates :notification_sender, format: { with: URI::MailTo::EMAIL_REGEXP, allow_blank: true }
validate :validate_urls_order
## Serializations
serialize :urls_order, Array
## Callbacks
before_save :check_urls_order_consistency
after_save :check_if_default_branch_changed
## Virtual attribute
attr_accessor :default_branch_has_changed
# Syntaxic sugar
def default_branch_has_changed?
default_branch_has_changed
end
private
def validate_urls_order
urls_order.each do |url|
errors.add(:urls_order, :invalid) unless ALLOWED_URLS.include?(url)
end
end
# 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_default_branch_changed
self.default_branch_has_changed = if default_branch_changed?
true
else
false
end
end
def check_urls_order_consistency
check_ssh_url
check_git_http_urls
check_go_url
check_git_url
check_git_annex_url
end
def check_ssh_url
git_ssh? ? add_url('ssh') : remove_url('ssh')
end
def check_git_http_urls
if git_http? && git_https?
add_url('http')
add_url('https')
elsif git_http?
add_url('http')
remove_url('https')
elsif git_https?
add_url('https')
remove_url('http')
else
remove_url('http')
remove_url('https')
end
end
def check_go_url
git_go? ? add_url('go') : remove_url('go')
end
def check_git_annex_url
git_annex? ? add_url('git_annex') : remove_url('git_annex')
end
def check_git_url
git_daemon? ? add_url('git') : remove_url('git')
end
def remove_url(url)
urls_order.delete(url)
end
def add_url(url)
urls_order.push(url).uniq!
end
end

View file

@ -0,0 +1,107 @@
class RepositoryMirror < ActiveRecord::Base
include Redmine::SafeAttributes
PUSHMODE_MIRROR = 0
PUSHMODE_FORCE = 1
PUSHMODE_FAST_FORWARD = 2
## Attributes
safe_attributes 'url', 'push_mode', 'include_all_branches', 'include_all_tags',
'explicit_refspec', 'active'
## Relations
belongs_to :repository
## Validations
validates :repository_id, presence: true
## Only allow SSH format
## ssh://git@redmine.example.org/project1/project2/project3/project4.git
## ssh://git@redmine.example.org:2222/project1/project2/project3/project4.git
validates :url, presence: true,
uniqueness: { case_sensitive: false, scope: :repository_id },
format: { with: RedmineGitHosting::Validators::GIT_SSH_URL_REGEX }
validates :push_mode, presence: true,
numericality: { only_integer: true },
inclusion: { in: [PUSHMODE_MIRROR, PUSHMODE_FORCE, PUSHMODE_FAST_FORWARD] }
## Additional validations
validate :mirror_configuration
## Scopes
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :has_explicit_refspec, -> { where(push_mode: '> 0') }
scope :sorted, -> { order(:url) }
## Callbacks
before_validation :strip_whitespace
def mirror_mode?
push_mode == PUSHMODE_MIRROR
end
def force_mode?
push_mode == PUSHMODE_FORCE
end
def push_mode_to_s
case push_mode
when 0
'mirror'
when 1
'force'
when 2
'fast_forward'
end
end
private
# Strip leading and trailing whitespace
def strip_whitespace
self.url = url.strip rescue ''
self.explicit_refspec = explicit_refspec.strip rescue ''
end
def mirror_configuration
if mirror_mode?
reset_fields
elsif include_all_branches? && include_all_tags?
mutual_exclusion_error
elsif explicit_refspec.present?
if include_all_branches?
errors.add(:explicit_refspec, "cannot be used with #{l(:label_mirror_include_all_branches)}.")
else
validate_refspec
end
elsif !include_all_branches? && !include_all_tags?
errors.add(:base, :nothing_to_push)
end
end
# Check format of refspec
#
def validate_refspec
RedmineGitHosting::Validators.valid_git_refspec_path?(explicit_refspec)
rescue RedmineGitHosting::Error::InvalidRefspec::BadFormat => e
errors.add(:explicit_refspec, :bad_format)
rescue RedmineGitHosting::Error::InvalidRefspec::NullComponent => e
errors.add(:explicit_refspec, :have_null_component)
end
def reset_fields
# clear out all extra parameters.. (we use javascript to hide them anyway)
self.include_all_branches = false
self.include_all_tags = false
self.explicit_refspec = ''
end
def mutual_exclusion_error
errors.add(:base, "Cannot #{l(:label_mirror_include_all_branches)} and #{l(:label_mirror_include_all_tags)} at the same time.")
return if explicit_refspec.blank?
errors.add(:explicit_refspec, "cannot be used with #{l(:label_mirror_include_all_branches)} or #{l(:label_mirror_include_all_tags)}")
end
end

View file

@ -0,0 +1,61 @@
require 'uri'
class RepositoryPostReceiveUrl < ActiveRecord::Base
include Redmine::SafeAttributes
## Attributes
safe_attributes 'url', 'mode', 'active', 'use_triggers', 'triggers', 'split_payloads'
## Relations
belongs_to :repository
## Validations
validates :repository_id, presence: true
# Only allow HTTP(s) format
validates :url, presence: true,
uniqueness: { case_sensitive: false, scope: :repository_id },
format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
validates :mode, presence: true, inclusion: { in: %i[github get post] }
## Serializations
serialize :triggers, Array
## Scopes
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :sorted, -> { order(:url) }
## Callbacks
before_validation :strip_whitespace
before_validation :remove_blank_triggers
def mode
self[:mode].to_sym
end
def mode=(value)
self[:mode] = value.to_s
end
def github_mode?
mode == :github
end
private
# Strip leading and trailing whitespace
def strip_whitespace
self.url = begin
url.strip
rescue StandardError
''
end
end
# Remove blank entries in triggers
def remove_blank_triggers
self.triggers = triggers.select(&:present?)
end
end

View file

@ -0,0 +1,48 @@
class RepositoryProtectedBranche < ActiveRecord::Base
include Redmine::SafeAttributes
VALID_PERMS = ['RW+', 'RW', 'R', '-'].freeze
DEFAULT_PERM = 'RW+'.freeze
acts_as_positioned
## Attributes
safe_attributes 'path', 'permissions', 'position'
## Relations
belongs_to :repository
has_many :protected_branches_members, foreign_key: :protected_branch_id, dependent: :destroy
has_many :members, through: :protected_branches_members, source: :principal
## Validations
validates :repository_id, presence: true
validates :path, presence: true, uniqueness: { scope: %i[permissions repository_id] }
validates :permissions, presence: true, inclusion: { in: VALID_PERMS }
## Scopes
default_scope { order(position: :asc) }
scope :sorted, -> { order(:path) }
class << self
def clone_from(parent)
parent = find_by(id: parent) unless parent.is_a? RepositoryProtectedBranche
copy = new
copy.attributes = parent.attributes
copy.repository = parent.repository
copy
end
end
# Accessors
#
def users
members.select { |m| m.class.name == 'User' }.uniq
end
def groups
members.select { |m| m.class.name == 'Group' }.uniq
end
def allowed_users
users.map(&:gitolite_identifier).sort
end
end