Commit e94f6229 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'fj-39176-create-project-snippet-repository' into 'master'

Add routes to allow git actions on snippet repositories

Closes #205666

See merge request gitlab-org/gitlab!21739
parents 798bb7ed e8ca2229
...@@ -6,7 +6,7 @@ module Repositories ...@@ -6,7 +6,7 @@ module Repositories
include KerberosSpnegoHelper include KerberosSpnegoHelper
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :authentication_result, :redirected_path attr_reader :authentication_result, :redirected_path, :container
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
...@@ -81,7 +81,7 @@ module Repositories ...@@ -81,7 +81,7 @@ module Repositories
end end
def parse_repo_path def parse_repo_path
@project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:repository_id]}") @container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:repository_id]}")
end end
def render_missing_personal_access_token def render_missing_personal_access_token
...@@ -93,7 +93,7 @@ module Repositories ...@@ -93,7 +93,7 @@ module Repositories
def repository def repository
strong_memoize(:repository) do strong_memoize(:repository) do
repo_type.repository_for(project) repo_type.repository_for(container)
end end
end end
...@@ -117,7 +117,8 @@ module Repositories ...@@ -117,7 +117,8 @@ module Repositories
def http_download_allowed? def http_download_allowed?
Gitlab::ProtocolAccess.allowed?('http') && Gitlab::ProtocolAccess.allowed?('http') &&
download_request? && download_request? &&
project && Guest.can?(:download_code, project) container &&
Guest.can?(repo_type.guest_read_ability, container)
end end
end end
end end
......
...@@ -84,10 +84,10 @@ module Repositories ...@@ -84,10 +84,10 @@ module Repositories
end end
def access def access
@access ||= access_klass.new(access_actor, project, 'http', @access ||= access_klass.new(access_actor, container, 'http',
authentication_abilities: authentication_abilities, authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id], namespace_path: params[:namespace_id],
project_path: project_path, repository_path: repository_path,
redirected_path: redirected_path, redirected_path: redirected_path,
auth_result_type: auth_result_type) auth_result_type: auth_result_type)
end end
...@@ -99,15 +99,18 @@ module Repositories ...@@ -99,15 +99,18 @@ module Repositories
def access_check def access_check
access.check(git_command, Gitlab::GitAccess::ANY) access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
if repo_type.project? && !container
@project = @container = access.project
end
end end
def access_klass def access_klass
@access_klass ||= repo_type.access_checker_class @access_klass ||= repo_type.access_checker_class
end end
def project_path def repository_path
@project_path ||= params[:repository_id].sub(/\.git$/, '') @repository_path ||= params[:repository_id].sub(/\.git$/, '')
end end
def log_user_activity def log_user_activity
......
...@@ -160,6 +160,10 @@ class Snippet < ApplicationRecord ...@@ -160,6 +160,10 @@ class Snippet < ApplicationRecord
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end end
def self.find_by_id_and_project(id:, project:)
Snippet.find_by(id: id, project: project)
end
def initialize(attributes = {}) def initialize(attributes = {})
# We can't use default_value_for because the database has a default # We can't use default_value_for because the database has a default
# value of 0 for visibility_level. If someone attempts to create a # value of 0 for visibility_level. If someone attempts to create a
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
# #
# Used for scheduling related jobs after a push action has been performed # Used for scheduling related jobs after a push action has been performed
class PostReceiveService class PostReceiveService
attr_reader :user, :project, :params attr_reader :user, :repository, :project, :params
def initialize(user, project, params) def initialize(user, repository, project, params)
@user = user @user = user
@repository = repository
@project = project @project = project
@params = params @params = params
end end
...@@ -24,7 +25,7 @@ class PostReceiveService ...@@ -24,7 +25,7 @@ class PostReceiveService
mr_options = push_options.get(:merge_request) mr_options = push_options.get(:merge_request)
if mr_options.present? if mr_options.present?
message = process_mr_push_options(mr_options, project, user, params[:changes]) message = process_mr_push_options(mr_options, params[:changes])
response.add_alert_message(message) response.add_alert_message(message)
end end
...@@ -46,8 +47,13 @@ class PostReceiveService ...@@ -46,8 +47,13 @@ class PostReceiveService
response response
end end
def process_mr_push_options(push_options, project, user, changes) def process_mr_push_options(push_options, changes)
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/61359') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/61359')
return unless repository
unless repository.repo_type.project?
return push_options_warning('Push options are only supported for projects')
end
service = ::MergeRequests::PushOptionsHandlerService.new( service = ::MergeRequests::PushOptionsHandlerService.new(
project, user, changes, push_options project, user, changes, push_options
...@@ -64,6 +70,8 @@ class PostReceiveService ...@@ -64,6 +70,8 @@ class PostReceiveService
end end
def merge_request_urls def merge_request_urls
return [] unless repository&.repo_type&.project?
::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end end
end end
...@@ -9,9 +9,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -9,9 +9,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
weight 5 weight 5
def perform(gl_repository, identifier, changes, push_options = {}) def perform(gl_repository, identifier, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository) container, project, repo_type = Gitlab::GlRepository.parse(gl_repository)
if project.nil? if project.nil? && (!repo_type.snippet? || container.is_a?(ProjectSnippet))
log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"") log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"")
return false return false
end end
...@@ -20,12 +20,14 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -20,12 +20,14 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
# Use Sidekiq.logger so arguments can be correlated with execution # Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's. # time and thread ID's.
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
post_received = Gitlab::GitPostReceive.new(project, identifier, changes, push_options) post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
if repo_type.wiki? if repo_type.wiki?
process_wiki_changes(post_received) process_wiki_changes(post_received, container)
elsif repo_type.project? elsif repo_type.project?
process_project_changes(post_received) process_project_changes(post_received, container)
elsif repo_type.snippet?
process_snippet_changes(post_received, container)
else else
# Other repos don't have hooks for now # Other repos don't have hooks for now
end end
...@@ -39,24 +41,50 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -39,24 +41,50 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end end
end end
def process_project_changes(post_received) def process_project_changes(post_received, project)
user = identify_user(post_received) user = identify_user(post_received)
return false unless user return false unless user
project = post_received.project
push_options = post_received.push_options push_options = post_received.push_options
changes = post_received.changes changes = post_received.changes
# We only need to expire certain caches once per push # We only need to expire certain caches once per push
expire_caches(post_received, post_received.project.repository) expire_caches(post_received, project.repository)
enqueue_repository_cache_update(post_received) enqueue_project_cache_update(post_received, project)
process_ref_changes(project, user, push_options: push_options, changes: changes) process_ref_changes(project, user, push_options: push_options, changes: changes)
update_remote_mirrors(post_received) update_remote_mirrors(post_received, project)
after_project_changes_hooks(project, user, changes.refs, changes.repository_data) after_project_changes_hooks(project, user, changes.refs, changes.repository_data)
end end
def process_wiki_changes(post_received, project)
project.touch(:last_activity_at, :last_repository_updated_at)
project.wiki.repository.expire_statistics_caches
ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
user = identify_user(post_received)
return false unless user
# We only need to expire certain caches once per push
expire_caches(post_received, project.wiki.repository)
::Git::WikiPushService.new(project, user, changes: post_received.changes).execute
end
def process_snippet_changes(post_received, snippet)
user = identify_user(post_received)
return false unless user
# At the moment, we only expires the repository caches.
# In the future we might need to call ProjectCacheWorker
# (or the custom class we create) to update the snippet
# repository size or any other key.
# We might also need to update the repository statistics.
expire_caches(post_received, snippet.repository)
end
# Expire the repository status, branch, and tag cache once per push. # Expire the repository status, branch, and tag cache once per push.
def expire_caches(post_received, repository) def expire_caches(post_received, repository)
repository.expire_status_cache if repository.empty? repository.expire_status_cache if repository.empty?
...@@ -65,12 +93,12 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -65,12 +93,12 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end end
# Schedule an update for the repository size and commit count if necessary. # Schedule an update for the repository size and commit count if necessary.
def enqueue_repository_cache_update(post_received) def enqueue_project_cache_update(post_received, project)
stats_to_invalidate = [:repository_size] stats_to_invalidate = [:repository_size]
stats_to_invalidate << :commit_count if post_received.includes_default_branch? stats_to_invalidate << :commit_count if post_received.includes_default_branch?
ProjectCacheWorker.perform_async( ProjectCacheWorker.perform_async(
post_received.project.id, project.id,
[], [],
stats_to_invalidate, stats_to_invalidate,
true true
...@@ -83,10 +111,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -83,10 +111,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
Git::ProcessRefChangesService.new(project, user, params).execute Git::ProcessRefChangesService.new(project, user, params).execute
end end
def update_remote_mirrors(post_received) def update_remote_mirrors(post_received, project)
return unless post_received.includes_branches? || post_received.includes_tags? return unless post_received.includes_branches? || post_received.includes_tags?
project = post_received.project
return unless project.has_remote_mirror? return unless project.has_remote_mirror?
project.mark_stuck_remote_mirrors_as_failed! project.mark_stuck_remote_mirrors_as_failed!
...@@ -99,20 +126,6 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker ...@@ -99,20 +126,6 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
end end
def process_wiki_changes(post_received)
post_received.project.touch(:last_activity_at, :last_repository_updated_at)
post_received.project.wiki.repository.expire_statistics_caches
ProjectCacheWorker.perform_async(post_received.project.id, [], [:wiki_size])
user = identify_user(post_received)
return false unless user
# We only need to expire certain caches once per push
expire_caches(post_received, post_received.project.wiki.repository)
::Git::WikiPushService.new(post_received.project, user, changes: post_received.changes).execute
end
def log(message) def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}") Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end end
......
---
title: Update git workflows and routes to allow snippets
merge_request: 21739
author:
type: added
...@@ -32,6 +32,14 @@ concern :lfsable do ...@@ -32,6 +32,14 @@ concern :lfsable do
end end
end end
# Git route for personal and project snippets
scope(path: ':namespace_id/:repository_id',
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.personal_and_project_snippets_path_regex, repository_id: /\d+\.git/ },
module: :repositories) do
concerns :gitactionable
end
scope(path: '*namespace_id/:repository_id', scope(path: '*namespace_id/:repository_id',
format: nil, format: nil,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
......
...@@ -79,7 +79,7 @@ module EE ...@@ -79,7 +79,7 @@ module EE
end end
def repository_full_path def repository_full_path
File.join(params[:namespace_id], project_path) File.join(params[:namespace_id], repository_path)
end end
def decoded_authorization def decoded_authorization
......
...@@ -22,11 +22,11 @@ module EE ...@@ -22,11 +22,11 @@ module EE
end end
end end
def process_wiki_changes(post_received) def process_wiki_changes(post_received, project)
super super
if ::Gitlab::Geo.primary? if ::Gitlab::Geo.primary?
::Geo::RepositoryUpdatedService.new(post_received.project.wiki.repository).execute ::Geo::RepositoryUpdatedService.new(project.wiki.repository).execute
end end
end end
......
...@@ -674,7 +674,7 @@ describe Gitlab::GitAccess do ...@@ -674,7 +674,7 @@ describe Gitlab::GitAccess do
protocol, protocol,
authentication_abilities: authentication_abilities, authentication_abilities: authentication_abilities,
namespace_path: namespace_path, namespace_path: namespace_path,
project_path: project_path, repository_path: project_path,
redirected_path: redirected_path redirected_path: redirected_path
) )
end end
......
...@@ -3,6 +3,8 @@ require 'spec_helper' ...@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::GlRepository::RepoType do describe Gitlab::GlRepository::RepoType do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
describe Gitlab::GlRepository::DESIGN do describe Gitlab::GlRepository::DESIGN do
it_behaves_like 'a repo type' do it_behaves_like 'a repo type' do
...@@ -24,6 +26,8 @@ describe Gitlab::GlRepository::RepoType do ...@@ -24,6 +26,8 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(project.design_repository.full_path)).to be_truthy expect(described_class.valid?(project.design_repository.full_path)).to be_truthy
expect(described_class.valid?(project.repository.full_path)).to be_falsey expect(described_class.valid?(project.repository.full_path)).to be_falsey
expect(described_class.valid?(project.wiki.repository.full_path)).to be_falsey expect(described_class.valid?(project.wiki.repository.full_path)).to be_falsey
expect(described_class.valid?("snippets/#{personal_snippet.id}")).to be_falsey
expect(described_class.valid?("#{project.full_path}/snippets/#{project_snippet.id}")).to be_falsey
end end
end end
end end
...@@ -6,7 +6,7 @@ describe Gitlab::GlRepository do ...@@ -6,7 +6,7 @@ describe Gitlab::GlRepository do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
it 'parses a design gl_repository' do it 'parses a design gl_repository' do
expect(described_class.parse("design-#{project.id}")).to eq([project, EE::Gitlab::GlRepository::DESIGN]) expect(described_class.parse("design-#{project.id}")).to eq([project, project, EE::Gitlab::GlRepository::DESIGN])
end end
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module API module API
module Helpers module Helpers
module InternalHelpers module InternalHelpers
attr_reader :redirected_path attr_reader :redirected_path, :container
delegate :wiki?, to: :repo_type delegate :wiki?, to: :repo_type
...@@ -22,10 +22,10 @@ module API ...@@ -22,10 +22,10 @@ module API
end end
def access_checker_for(actor, protocol) def access_checker_for(actor, protocol)
access_checker_klass.new(actor.key_or_user, project, protocol, access_checker_klass.new(actor.key_or_user, container, protocol,
authentication_abilities: ssh_authentication_abilities, authentication_abilities: ssh_authentication_abilities,
namespace_path: namespace_path, namespace_path: namespace_path,
project_path: project_path, repository_path: project_path,
redirected_path: redirected_path) redirected_path: redirected_path)
end end
...@@ -80,7 +80,7 @@ module API ...@@ -80,7 +80,7 @@ module API
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project def set_project
@project, @repo_type, @redirected_path = @container, @project, @repo_type, @redirected_path =
if params[:gl_repository] if params[:gl_repository]
Gitlab::GlRepository.parse(params[:gl_repository]) Gitlab::GlRepository.parse(params[:gl_repository])
elsif params[:project] elsif params[:project]
...@@ -92,17 +92,17 @@ module API ...@@ -92,17 +92,17 @@ module API
# Project id to pass between components that don't share/don't have # Project id to pass between components that don't share/don't have
# access to the same filesystem mounts # access to the same filesystem mounts
def gl_repository def gl_repository
repo_type.identifier_for_container(project) repo_type.identifier_for_container(container)
end end
def gl_project_path def gl_repository_path
repository.full_path repository.full_path
end end
# Return the repository depending on whether we want the wiki or the # Return the repository depending on whether we want the wiki or the
# regular repository # regular repository
def repository def repository
@repository ||= repo_type.repository_for(project) @repository ||= repo_type.repository_for(container)
end end
# Return the Gitaly Address if it is enabled # Return the Gitaly Address if it is enabled
...@@ -111,8 +111,8 @@ module API ...@@ -111,8 +111,8 @@ module API
{ {
repository: repository.gitaly_repository, repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(container.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage), token: Gitlab::GitalyClient.token(container.repository_storage),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags
} }
end end
......
...@@ -67,7 +67,7 @@ module API ...@@ -67,7 +67,7 @@ module API
when ::Gitlab::GitAccessResult::Success when ::Gitlab::GitAccessResult::Success
payload = { payload = {
gl_repository: gl_repository, gl_repository: gl_repository,
gl_project_path: gl_project_path, gl_project_path: gl_repository_path,
gl_id: Gitlab::GlId.gl_id(actor.user), gl_id: Gitlab::GlId.gl_id(actor.user),
gl_username: actor.username, gl_username: actor.username,
git_config_options: [], git_config_options: [],
...@@ -216,7 +216,7 @@ module API ...@@ -216,7 +216,7 @@ module API
post '/post_receive' do post '/post_receive' do
status 200 status 200
response = PostReceiveService.new(actor.user, project, params).execute response = PostReceiveService.new(actor.user, repository, project, params).execute
ee_post_receive_response_hook(response) ee_post_receive_response_hook(response)
......
...@@ -43,15 +43,15 @@ module Gitlab ...@@ -43,15 +43,15 @@ module Gitlab
PUSH_COMMANDS = %w{git-receive-pack}.freeze PUSH_COMMANDS = %w{git-receive-pack}.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :repository_path, :redirected_path, :auth_result_type, :changes, :logger
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil) def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, repository_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@authentication_abilities = authentication_abilities @authentication_abilities = Array(authentication_abilities)
@namespace_path = namespace_path || project&.namespace&.full_path @namespace_path = namespace_path || project&.namespace&.full_path
@project_path = project_path || project&.path @repository_path = repository_path || project&.path
@redirected_path = redirected_path @redirected_path = redirected_path
@auth_result_type = auth_result_type @auth_result_type = auth_result_type
end end
...@@ -224,7 +224,7 @@ module Gitlab ...@@ -224,7 +224,7 @@ module Gitlab
return unless user&.can?(:create_projects, namespace) return unless user&.can?(:create_projects, namespace)
project_params = { project_params = {
path: project_path, path: repository_path,
namespace_id: namespace.id, namespace_id: namespace.id,
visibility_level: Gitlab::VisibilityLevel::PRIVATE visibility_level: Gitlab::VisibilityLevel::PRIVATE
} }
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
module Gitlab module Gitlab
class GitPostReceive class GitPostReceive
include Gitlab::Identifier include Gitlab::Identifier
attr_reader :project, :identifier, :changes, :push_options attr_reader :container, :identifier, :changes, :push_options
def initialize(project, identifier, changes, push_options = {}) def initialize(container, identifier, changes, push_options = {})
@project = project @container = container
@identifier = identifier @identifier = identifier
@changes = parse_changes(changes) @changes = parse_changes(changes)
@push_options = push_options @push_options = push_options
...@@ -27,10 +27,10 @@ module Gitlab ...@@ -27,10 +27,10 @@ module Gitlab
def includes_default_branch? def includes_default_branch?
# If the branch doesn't have a default branch yet, we presume the # If the branch doesn't have a default branch yet, we presume the
# first branch pushed will be the default. # first branch pushed will be the default.
return true unless project.default_branch.present? return true unless container.default_branch.present?
changes.branch_changes.any? do |change| changes.branch_changes.any? do |change|
Gitlab::Git.branch_name(change[:ref]) == project.default_branch Gitlab::Git.branch_name(change[:ref]) == container.default_branch
end end
end end
......
...@@ -7,19 +7,21 @@ module Gitlab ...@@ -7,19 +7,21 @@ module Gitlab
PROJECT = RepoType.new( PROJECT = RepoType.new(
name: :project, name: :project,
access_checker_class: Gitlab::GitAccess, access_checker_class: Gitlab::GitAccess,
repository_resolver: -> (project) { project.repository } repository_resolver: -> (project) { project&.repository }
).freeze ).freeze
WIKI = RepoType.new( WIKI = RepoType.new(
name: :wiki, name: :wiki,
access_checker_class: Gitlab::GitAccessWiki, access_checker_class: Gitlab::GitAccessWiki,
repository_resolver: -> (project) { project.wiki.repository }, repository_resolver: -> (project) { project&.wiki&.repository },
suffix: :wiki suffix: :wiki
).freeze ).freeze
SNIPPET = RepoType.new( SNIPPET = RepoType.new(
name: :snippet, name: :snippet,
access_checker_class: Gitlab::GitAccessSnippet, access_checker_class: Gitlab::GitAccessSnippet,
repository_resolver: -> (snippet) { snippet.repository }, repository_resolver: -> (snippet) { snippet&.repository },
container_resolver: -> (id) { Snippet.find_by_id(id) } container_resolver: -> (id) { Snippet.find_by_id(id) },
project_resolver: -> (snippet) { snippet&.project },
guest_read_ability: :read_snippet
).freeze ).freeze
TYPES = { TYPES = {
...@@ -42,7 +44,7 @@ module Gitlab ...@@ -42,7 +44,7 @@ module Gitlab
container = type.fetch_container!(gl_repository) container = type.fetch_container!(gl_repository)
[container, type] [container, type.project_for(container), type]
end end
def self.default_type def self.default_type
......
...@@ -7,6 +7,8 @@ module Gitlab ...@@ -7,6 +7,8 @@ module Gitlab
:access_checker_class, :access_checker_class,
:repository_resolver, :repository_resolver,
:container_resolver, :container_resolver,
:project_resolver,
:guest_read_ability,
:suffix :suffix
def initialize( def initialize(
...@@ -14,11 +16,15 @@ module Gitlab ...@@ -14,11 +16,15 @@ module Gitlab
access_checker_class:, access_checker_class:,
repository_resolver:, repository_resolver:,
container_resolver: default_container_resolver, container_resolver: default_container_resolver,
project_resolver: nil,
guest_read_ability: :download_code,
suffix: nil) suffix: nil)
@name = name @name = name
@access_checker_class = access_checker_class @access_checker_class = access_checker_class
@repository_resolver = repository_resolver @repository_resolver = repository_resolver
@container_resolver = container_resolver @container_resolver = container_resolver
@project_resolver = project_resolver
@guest_read_ability = guest_read_ability
@suffix = suffix @suffix = suffix
end end
...@@ -59,8 +65,18 @@ module Gitlab ...@@ -59,8 +65,18 @@ module Gitlab
repository_resolver.call(container) repository_resolver.call(container)
end end
def project_for(container)
return container unless project_resolver
project_resolver.call(container)
end
def valid?(repository_path) def valid?(repository_path)
repository_path.end_with?(path_suffix) repository_path.end_with?(path_suffix) &&
(
!snippet? ||
repository_path.match?(Gitlab::PathRegex.full_snippets_repository_path_regex)
)
end end
private private
......
...@@ -237,8 +237,32 @@ module Gitlab ...@@ -237,8 +237,32 @@ module Gitlab
}x }x
end end
def full_snippets_repository_path_regex
%r{\A(#{personal_snippet_repository_path_regex}|#{project_snippet_repository_path_regex})\z}
end
def personal_and_project_snippets_path_regex
%r{#{personal_snippet_path_regex}|#{project_snippet_path_regex}}
end
private private
def personal_snippet_path_regex
/snippets/
end
def personal_snippet_repository_path_regex
%r{#{personal_snippet_path_regex}/\d+}
end
def project_snippet_path_regex
%r{#{full_namespace_route_regex}/#{project_route_regex}/snippets}
end
def project_snippet_repository_path_regex
%r{#{project_snippet_path_regex}/\d+}
end
def single_line_regexp(regex) def single_line_regexp(regex)
# Turns a multiline extended regexp into a single line one, # Turns a multiline extended regexp into a single line one,
# because `rake routes` breaks on multiline regexes. # because `rake routes` breaks on multiline regexes.
......
...@@ -19,22 +19,33 @@ module Gitlab ...@@ -19,22 +19,33 @@ module Gitlab
# Removing the suffix (.wiki, .design, ...) from the project path # Removing the suffix (.wiki, .design, ...) from the project path
full_path = repo_path.chomp(type.path_suffix) full_path = repo_path.chomp(type.path_suffix)
container, project, was_redirected = find_container(type, full_path)
project, was_redirected = find_project(full_path)
redirected_path = repo_path if was_redirected redirected_path = repo_path if was_redirected
# If we found a matching project, then the type was matched, no need to return [container, project, type, redirected_path] if container
# continue looking.
return [project, type, redirected_path] if project
end end
# When a project did not exist, the parsed repo_type would be empty. # When a project did not exist, the parsed repo_type would be empty.
# In that case, we want to continue with a regular project repository. As we # In that case, we want to continue with a regular project repository. As we
# could create the project if the user pushing is allowed to do so. # could create the project if the user pushing is allowed to do so.
[nil, Gitlab::GlRepository.default_type, nil] [nil, nil, Gitlab::GlRepository.default_type, nil]
end
def self.find_container(type, full_path)
if type.snippet?
snippet, was_redirected = find_snippet(full_path)
[snippet, snippet&.project, was_redirected]
else
project, was_redirected = find_project(full_path)
[project, project, was_redirected]
end
end end
def self.find_project(project_path) def self.find_project(project_path)
return [nil, false] if project_path.blank?
project = Project.find_by_full_path(project_path, follow_redirects: true) project = Project.find_by_full_path(project_path, follow_redirects: true)
[project, redirected?(project, project_path)] [project, redirected?(project, project_path)]
...@@ -43,6 +54,27 @@ module Gitlab ...@@ -43,6 +54,27 @@ module Gitlab
def self.redirected?(project, project_path) def self.redirected?(project, project_path)
project && project.full_path.casecmp(project_path) != 0 project && project.full_path.casecmp(project_path) != 0
end end
# Snippet_path can be either:
# - snippets/1
# - h5bp/html5-boilerplate/snippets/53
def self.find_snippet(snippet_path)
return [nil, false] if snippet_path.blank?
snippet_id, project_path = extract_snippet_info(snippet_path)
project, was_redirected = find_project(project_path)
[Snippet.find_by_id_and_project(id: snippet_id, project: project), was_redirected]
end
def self.extract_snippet_info(snippet_path)
path_segments = snippet_path.split('/')
snippet_id = path_segments.pop
path_segments.pop # Remove snippets from path
project_path = File.join(path_segments)
[snippet_id, project_path]
end
end end
end end
......
...@@ -24,7 +24,7 @@ module Gitlab ...@@ -24,7 +24,7 @@ module Gitlab
attrs = { attrs = {
GL_ID: Gitlab::GlId.gl_id(user), GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: repo_type.identifier_for_container(repository.project), GL_REPOSITORY: repo_type.identifier_for_container(repository.container),
GL_USERNAME: user&.username, GL_USERNAME: user&.username,
ShowAllRefs: show_all_refs, ShowAllRefs: show_all_refs,
Repository: repository.gitaly_repository.to_h, Repository: repository.gitaly_repository.to_h,
......
...@@ -6,16 +6,18 @@ describe Repositories::GitHttpController do ...@@ -6,16 +6,18 @@ describe Repositories::GitHttpController do
include GitHttpHelpers include GitHttpHelpers
let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
let(:namespace_id) { project.namespace.to_param } let(:namespace_id) { project.namespace.to_param }
let(:repository_id) { project.path + '.git' } let(:repository_id) { project.path + '.git' }
let(:project_params) do let(:container_params) do
{ {
namespace_id: namespace_id, namespace_id: namespace_id,
repository_id: repository_id repository_id: repository_id
} }
end end
let(:params) { project_params } let(:params) { container_params }
describe 'HEAD #info_refs' do describe 'HEAD #info_refs' do
it 'returns 403' do it 'returns 403' do
...@@ -27,7 +29,7 @@ describe Repositories::GitHttpController do ...@@ -27,7 +29,7 @@ describe Repositories::GitHttpController do
shared_examples 'info_refs behavior' do shared_examples 'info_refs behavior' do
describe 'GET #info_refs' do describe 'GET #info_refs' do
let(:params) { project_params.merge(service: 'git-upload-pack') } let(:params) { container_params.merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh') stub_application_setting(enabled_git_access_protocol: 'ssh')
...@@ -41,8 +43,6 @@ describe Repositories::GitHttpController do ...@@ -41,8 +43,6 @@ describe Repositories::GitHttpController do
end end
context 'with authorized user' do context 'with authorized user' do
let(:user) { project.owner }
before do before do
request.headers.merge! auth_env(user.username, user.password, nil) request.headers.merge! auth_env(user.username, user.password, nil)
end end
...@@ -122,7 +122,7 @@ describe Repositories::GitHttpController do ...@@ -122,7 +122,7 @@ describe Repositories::GitHttpController do
end end
shared_examples 'access checker class' do shared_examples 'access checker class' do
let(:params) { project_params.merge(service: 'git-upload-pack') } let(:params) { container_params.merge(service: 'git-upload-pack') }
it 'calls the right access class checker with the right object' do it 'calls the right access class checker with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true) allow(controller).to receive(:verify_workhorse_api!).and_return(true)
...@@ -136,11 +136,41 @@ describe Repositories::GitHttpController do ...@@ -136,11 +136,41 @@ describe Repositories::GitHttpController do
end end
context 'when repository container is a project' do context 'when repository container is a project' do
it_behaves_like 'info_refs behavior' it_behaves_like 'info_refs behavior' do
let(:user) { project.owner }
end
it_behaves_like 'git_upload_pack behavior', true it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess } let(:expected_class) { Gitlab::GitAccess }
let(:expected_object) { project } let(:expected_object) { project }
end end
end end
context 'when repository container is a personal snippet' do
let(:namespace_id) { 'snippets' }
let(:repository_id) { personal_snippet.to_param + '.git' }
it_behaves_like 'info_refs behavior' do
let(:user) { personal_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { personal_snippet }
end
end
context 'when repository container is a project snippet' do
let(:namespace_id) { project.full_path + '/snippets' }
let(:repository_id) { project_snippet.to_param + '.git' }
it_behaves_like 'info_refs behavior' do
let(:user) { project_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { project_snippet }
end
end
end end
...@@ -240,7 +240,7 @@ describe Gitlab::GitAccess do ...@@ -240,7 +240,7 @@ describe Gitlab::GitAccess do
let(:access) do let(:access) do
described_class.new(actor, nil, described_class.new(actor, nil,
protocol, authentication_abilities: authentication_abilities, protocol, authentication_abilities: authentication_abilities,
project_path: project_path, namespace_path: namespace_path, repository_path: project_path, namespace_path: namespace_path,
redirected_path: redirected_path) redirected_path: redirected_path)
end end
...@@ -259,7 +259,7 @@ describe Gitlab::GitAccess do ...@@ -259,7 +259,7 @@ describe Gitlab::GitAccess do
let(:access) do let(:access) do
described_class.new(actor, nil, described_class.new(actor, nil,
protocol, authentication_abilities: authentication_abilities, protocol, authentication_abilities: authentication_abilities,
project_path: project_path, namespace_path: namespace_path, repository_path: project_path, namespace_path: namespace_path,
redirected_path: redirected_path) redirected_path: redirected_path)
end end
...@@ -453,7 +453,7 @@ describe Gitlab::GitAccess do ...@@ -453,7 +453,7 @@ describe Gitlab::GitAccess do
let(:access) do let(:access) do
described_class.new(actor, project, described_class.new(actor, project,
protocol, authentication_abilities: authentication_abilities, protocol, authentication_abilities: authentication_abilities,
project_path: project_path, namespace_path: namespace_path, repository_path: project_path, namespace_path: namespace_path,
redirected_path: redirected_path) redirected_path: redirected_path)
end end
...@@ -598,7 +598,7 @@ describe Gitlab::GitAccess do ...@@ -598,7 +598,7 @@ describe Gitlab::GitAccess do
let(:public_project) { create(:project, :public, :repository) } let(:public_project) { create(:project, :public, :repository) }
let(:project_path) { public_project.path } let(:project_path) { public_project.path }
let(:namespace_path) { public_project.namespace.path } let(:namespace_path) { public_project.namespace.path }
let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], project_path: project_path, namespace_path: namespace_path) } let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], repository_path: project_path, namespace_path: namespace_path) }
context 'when repository is enabled' do context 'when repository is enabled' do
it 'give access to download code' do it 'give access to download code' do
...@@ -1203,7 +1203,7 @@ describe Gitlab::GitAccess do ...@@ -1203,7 +1203,7 @@ describe Gitlab::GitAccess do
def access def access
described_class.new(actor, project, protocol, described_class.new(actor, project, protocol,
authentication_abilities: authentication_abilities, authentication_abilities: authentication_abilities,
namespace_path: namespace_path, project_path: project_path, namespace_path: namespace_path, repository_path: project_path,
redirected_path: redirected_path, auth_result_type: auth_result_type) redirected_path: redirected_path, auth_result_type: auth_result_type)
end end
......
...@@ -5,46 +5,62 @@ describe Gitlab::GlRepository::RepoType do ...@@ -5,46 +5,62 @@ describe Gitlab::GlRepository::RepoType do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) } let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) } let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
let(:project_path) { project.repository.full_path }
let(:wiki_path) { project.wiki.repository.full_path }
let(:personal_snippet_path) { "snippets/#{personal_snippet.id}" }
let(:project_snippet_path) { "#{project.full_path}/snippets/#{project_snippet.id}" }
describe Gitlab::GlRepository::PROJECT do describe Gitlab::GlRepository::PROJECT do
it_behaves_like 'a repo type' do it_behaves_like 'a repo type' do
let(:expected_identifier) { "project-#{project.id}" }
let(:expected_id) { project.id.to_s } let(:expected_id) { project.id.to_s }
let(:expected_identifier) { "project-#{expected_id}" }
let(:expected_suffix) { '' } let(:expected_suffix) { '' }
let(:expected_repository) { project.repository }
let(:expected_container) { project } let(:expected_container) { project }
let(:expected_repository) { expected_container.repository }
end end
it 'knows its type' do it 'knows its type' do
expect(described_class).not_to be_wiki aggregate_failures do
expect(described_class).to be_project expect(described_class).not_to be_wiki
expect(described_class).not_to be_snippet expect(described_class).to be_project
expect(described_class).not_to be_snippet
end
end end
it 'checks if repository path is valid' do it 'checks if repository path is valid' do
expect(described_class.valid?(project.repository.full_path)).to be_truthy aggregate_failures do
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy expect(described_class.valid?(project_path)).to be_truthy
expect(described_class.valid?(wiki_path)).to be_truthy
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
end
end end
end end
describe Gitlab::GlRepository::WIKI do describe Gitlab::GlRepository::WIKI do
it_behaves_like 'a repo type' do it_behaves_like 'a repo type' do
let(:expected_identifier) { "wiki-#{project.id}" }
let(:expected_id) { project.id.to_s } let(:expected_id) { project.id.to_s }
let(:expected_identifier) { "wiki-#{expected_id}" }
let(:expected_suffix) { '.wiki' } let(:expected_suffix) { '.wiki' }
let(:expected_repository) { project.wiki.repository }
let(:expected_container) { project } let(:expected_container) { project }
let(:expected_repository) { expected_container.wiki.repository }
end end
it 'knows its type' do it 'knows its type' do
expect(described_class).to be_wiki aggregate_failures do
expect(described_class).not_to be_project expect(described_class).to be_wiki
expect(described_class).not_to be_snippet expect(described_class).not_to be_project
expect(described_class).not_to be_snippet
end
end end
it 'checks if repository path is valid' do it 'checks if repository path is valid' do
expect(described_class.valid?(project.repository.full_path)).to be_falsey aggregate_failures do
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy expect(described_class.valid?(project_path)).to be_falsey
expect(described_class.valid?(wiki_path)).to be_truthy
expect(described_class.valid?(personal_snippet_path)).to be_falsey
expect(described_class.valid?(project_snippet_path)).to be_falsey
end
end end
end end
...@@ -59,9 +75,20 @@ describe Gitlab::GlRepository::RepoType do ...@@ -59,9 +75,20 @@ describe Gitlab::GlRepository::RepoType do
end end
it 'knows its type' do it 'knows its type' do
expect(described_class).to be_snippet aggregate_failures do
expect(described_class).not_to be_wiki expect(described_class).to be_snippet
expect(described_class).not_to be_project expect(described_class).not_to be_wiki
expect(described_class).not_to be_project
end
end
it 'checks if repository path is valid' do
aggregate_failures do
expect(described_class.valid?(project_path)).to be_falsey
expect(described_class.valid?(wiki_path)).to be_falsey
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
end
end end
end end
...@@ -75,9 +102,20 @@ describe Gitlab::GlRepository::RepoType do ...@@ -75,9 +102,20 @@ describe Gitlab::GlRepository::RepoType do
end end
it 'knows its type' do it 'knows its type' do
expect(described_class).to be_snippet aggregate_failures do
expect(described_class).not_to be_wiki expect(described_class).to be_snippet
expect(described_class).not_to be_project expect(described_class).not_to be_wiki
expect(described_class).not_to be_project
end
end
it 'checks if repository path is valid' do
aggregate_failures do
expect(described_class.valid?(project_path)).to be_falsey
expect(described_class.valid?(wiki_path)).to be_falsey
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
end
end end
end end
end end
......
...@@ -5,13 +5,18 @@ require 'spec_helper' ...@@ -5,13 +5,18 @@ require 'spec_helper'
describe ::Gitlab::GlRepository do describe ::Gitlab::GlRepository do
describe '.parse' do describe '.parse' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:snippet) { create(:personal_snippet) }
it 'parses a project gl_repository' do it 'parses a project gl_repository' do
expect(described_class.parse("project-#{project.id}")).to eq([project, Gitlab::GlRepository::PROJECT]) expect(described_class.parse("project-#{project.id}")).to eq([project, project, Gitlab::GlRepository::PROJECT])
end end
it 'parses a wiki gl_repository' do it 'parses a wiki gl_repository' do
expect(described_class.parse("wiki-#{project.id}")).to eq([project, Gitlab::GlRepository::WIKI]) expect(described_class.parse("wiki-#{project.id}")).to eq([project, project, Gitlab::GlRepository::WIKI])
end
it 'parses a snippet gl_repository' do
expect(described_class.parse("snippet-#{snippet.id}")).to eq([snippet, nil, Gitlab::GlRepository::SNIPPET])
end end
it 'throws an argument error on an invalid gl_repository type' do it 'throws an argument error on an invalid gl_repository type' do
......
...@@ -411,4 +411,37 @@ describe Gitlab::PathRegex do ...@@ -411,4 +411,37 @@ describe Gitlab::PathRegex do
it { is_expected.not_to match('git lab') } it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') } it { is_expected.not_to match('gitlab.git') }
end end
shared_examples 'invalid snippet routes' do
it { is_expected.not_to match('gitlab-org/gitlab/snippets/1.git') }
it { is_expected.not_to match('snippets/1.git') }
it { is_expected.not_to match('gitlab-org/gitlab/snippets/') }
it { is_expected.not_to match('/gitlab-org/gitlab/snippets/1') }
it { is_expected.not_to match('gitlab-org/gitlab/snippets/foo') }
it { is_expected.not_to match('root/snippets/1') }
it { is_expected.not_to match('/snippets/1') }
it { is_expected.not_to match('snippets/') }
it { is_expected.not_to match('snippets/foo') }
end
describe '.full_snippets_repository_path_regex' do
subject { described_class.full_snippets_repository_path_regex }
it { is_expected.to match('gitlab-org/gitlab/snippets/1') }
it { is_expected.to match('snippets/1') }
it_behaves_like 'invalid snippet routes'
end
describe '.personal_and_project_snippets_path_regex' do
subject { %r{\A#{described_class.personal_and_project_snippets_path_regex}\z} }
it { is_expected.to match('gitlab-org/gitlab/snippets') }
it { is_expected.to match('snippets') }
it { is_expected.not_to match('gitlab-org/gitlab/snippets/1') }
it { is_expected.not_to match('snippets/1') }
it_behaves_like 'invalid snippet routes'
end
end end
...@@ -3,60 +3,72 @@ ...@@ -3,60 +3,72 @@
require 'spec_helper' require 'spec_helper'
describe ::Gitlab::RepoPath do describe ::Gitlab::RepoPath do
describe '.parse' do include Gitlab::Routing
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet) }
let_it_be(:project_snippet) { create(:project_snippet, project: project) }
let_it_be(:redirect) { project.route.create_redirect('foo/bar/baz') }
describe '.parse' do
context 'a repository storage path' do context 'a repository storage path' do
it 'parses a full repository path' do it 'parses a full repository project path' do
expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil]) expect(described_class.parse(project.repository.full_path)).to eq([project, project, Gitlab::GlRepository::PROJECT, nil])
end
it 'parses a full wiki project path' do
expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, project, Gitlab::GlRepository::WIKI, nil])
end
it 'parses a personal snippet repository path' do
expect(described_class.parse("snippets/#{personal_snippet.id}")).to eq([personal_snippet, nil, Gitlab::GlRepository::SNIPPET, nil])
end end
it 'parses a full wiki path' do it 'parses a project snippet repository path' do
expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil]) expect(described_class.parse("#{project.full_path}/snippets/#{project_snippet.id}")).to eq([project_snippet, project, Gitlab::GlRepository::SNIPPET, nil])
end end
end end
context 'a relative path' do context 'a relative path' do
it 'parses a relative repository path' do it 'parses a relative repository path' do
expect(described_class.parse(project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) expect(described_class.parse(project.full_path + '.git')).to eq([project, project, Gitlab::GlRepository::PROJECT, nil])
end end
it 'parses a relative wiki path' do it 'parses a relative wiki path' do
expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, nil]) expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, nil])
end end
it 'parses a relative path starting with /' do it 'parses a relative path starting with /' do
expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, project, Gitlab::GlRepository::PROJECT, nil])
end end
context 'of a redirected project' do context 'of a redirected project' do
let(:redirect) { project.route.create_redirect('foo/bar') } let(:redirect) { project.route.create_redirect('foo/bar') }
it 'parses a relative repository path' do it 'parses a relative repository path' do
expect(described_class.parse(redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) expect(described_class.parse(redirect.path + '.git')).to eq([project, project, Gitlab::GlRepository::PROJECT, 'foo/bar'])
end end
it 'parses a relative wiki path' do it 'parses a relative wiki path' do
expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki']) expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki'])
end end
it 'parses a relative path starting with /' do it 'parses a relative path starting with /' do
expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, project, Gitlab::GlRepository::PROJECT, 'foo/bar'])
end
it 'parses a redirected project snippet repository path' do
expect(described_class.parse(redirect.path + "/snippets/#{project_snippet.id}.git")).to eq([project_snippet, project, Gitlab::GlRepository::SNIPPET, "foo/bar/snippets/#{project_snippet.id}"])
end end
end end
end end
it "returns the default type for non existent paths" do it 'returns the default type for non existent paths' do
_project, type, _redirected = described_class.parse("path/non-existent.git") expect(described_class.parse('path/non-existent.git')).to eq([nil, nil, Gitlab::GlRepository.default_type, nil])
expect(type).to eq(Gitlab::GlRepository.default_type)
end end
end end
describe '.find_project' do describe '.find_project' do
let(:project) { create(:project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') }
context 'when finding a project by its canonical path' do context 'when finding a project by its canonical path' do
context 'when the cases match' do context 'when the cases match' do
it 'returns the project and false' do it 'returns the project and false' do
...@@ -81,4 +93,34 @@ describe ::Gitlab::RepoPath do ...@@ -81,4 +93,34 @@ describe ::Gitlab::RepoPath do
end end
end end
end end
describe '.find_snippet' do
it 'extracts path and id from personal snippet route' do
expect(described_class.find_snippet("snippets/#{personal_snippet.id}")).to eq([personal_snippet, false])
end
it 'extracts path and id from project snippet route' do
expect(described_class.find_snippet("#{project.full_path}/snippets/#{project_snippet.id}")).to eq([project_snippet, false])
end
it 'returns nil for invalid snippet paths' do
aggregate_failures do
expect(described_class.find_snippet("snippets/#{project_snippet.id}")).to eq([nil, false])
expect(described_class.find_snippet("#{project.full_path}/snippets/#{personal_snippet.id}")).to eq([nil, false])
expect(described_class.find_snippet('')).to eq([nil, false])
end
end
it 'returns nil for snippets not associated with the project' do
snippet = create(:project_snippet)
expect(described_class.find_snippet("#{project.full_path}/snippets/#{snippet.id}")).to eq([nil, false])
end
context 'when finding a project snippet via a redirect' do
it 'returns the project and true' do
expect(described_class.find_snippet("#{redirect.path}/snippets/#{project_snippet.id}")).to eq([project_snippet, true])
end
end
end
end end
...@@ -12,19 +12,44 @@ describe Gitlab::RepositoryCache do ...@@ -12,19 +12,44 @@ describe Gitlab::RepositoryCache do
describe '#cache_key' do describe '#cache_key' do
subject { cache.cache_key(:foo) } subject { cache.cache_key(:foo) }
it 'includes the namespace' do shared_examples 'cache_key examples' do
expect(subject).to eq "foo:#{namespace}" it 'includes the namespace' do
expect(subject).to eq "foo:#{namespace}"
end
context 'with a given namespace' do
let(:extra_namespace) { 'my:data' }
let(:cache) do
described_class.new(repository, extra_namespace: extra_namespace,
backend: backend)
end
it 'includes the full namespace' do
expect(subject).to eq "foo:#{namespace}:#{extra_namespace}"
end
end
end end
context 'with a given namespace' do describe 'project repository' do
let(:extra_namespace) { 'my:data' } it_behaves_like 'cache_key examples' do
let(:cache) do let(:repository) { project.repository }
described_class.new(repository, extra_namespace: extra_namespace,
backend: backend)
end end
end
describe 'personal snippet repository' do
let_it_be(:personal_snippet) { create(:personal_snippet) }
let(:namespace) { repository.full_path }
it_behaves_like 'cache_key examples' do
let(:repository) { personal_snippet.repository }
end
end
describe 'project snippet repository' do
let_it_be(:project_snippet) { create(:project_snippet, project: project) }
it 'includes the full namespace' do it_behaves_like 'cache_key examples' do
expect(subject).to eq "foo:#{namespace}:#{extra_namespace}" let(:repository) { project_snippet.repository }
end end
end end
end end
......
...@@ -11,16 +11,41 @@ describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do ...@@ -11,16 +11,41 @@ describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
describe '#cache_key' do describe '#cache_key' do
subject { cache.cache_key(:foo) } subject { cache.cache_key(:foo) }
it 'includes the namespace' do shared_examples 'cache_key examples' do
is_expected.to eq("foo:#{namespace}:set") it 'includes the namespace' do
is_expected.to eq("foo:#{namespace}:set")
end
context 'with a given namespace' do
let(:extra_namespace) { 'my:data' }
let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
it 'includes the full namespace' do
is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set")
end
end
end
describe 'project repository' do
it_behaves_like 'cache_key examples' do
let(:repository) { project.repository }
end
end
describe 'personal snippet repository' do
let_it_be(:personal_snippet) { create(:personal_snippet) }
let(:namespace) { repository.full_path }
it_behaves_like 'cache_key examples' do
let(:repository) { personal_snippet.repository }
end
end end
context 'with a given namespace' do describe 'project snippet repository' do
let(:extra_namespace) { 'my:data' } let_it_be(:project_snippet) { create(:project_snippet, project: project) }
let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
it 'includes the full namespace' do it_behaves_like 'cache_key examples' do
is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set") let(:repository) { project_snippet.repository }
end end
end end
end end
......
This diff is collapsed.
This diff is collapsed.
...@@ -34,6 +34,31 @@ describe PostReceive do ...@@ -34,6 +34,31 @@ describe PostReceive do
expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
expect(perform).to be(false) expect(perform).to be(false)
end end
context 'with PersonalSnippet' do
let(:gl_repository) { "snippet-#{snippet.id}" }
let(:snippet) { create(:personal_snippet, author: project.owner) }
it 'does not log an error' do
expect(Gitlab::GitLogger).not_to receive(:error)
expect(Gitlab::GitPostReceive).to receive(:new).and_call_original
expect_any_instance_of(described_class) do |instance|
expect(instance).to receive(:process_snippet_changes)
end
perform
end
end
context 'with ProjectSnippet' do
let(:gl_repository) { "snippet-#{snippet.id}" }
let(:snippet) { create(:snippet, type: 'ProjectSnippet', project: nil, author: project.owner) }
it 'returns false and logs an error' do
expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
expect(perform).to be(false)
end
end
end end
describe "#process_project_changes" do describe "#process_project_changes" do
...@@ -44,7 +69,7 @@ describe PostReceive do ...@@ -44,7 +69,7 @@ describe PostReceive do
before do before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner) allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner)
# Need to mock here so we can expect calls on project # Need to mock here so we can expect calls on project
allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, Gitlab::GlRepository::PROJECT]) allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, empty_project, Gitlab::GlRepository::PROJECT])
end end
it 'expire the status cache' do it 'expire the status cache' do
...@@ -97,7 +122,7 @@ describe PostReceive do ...@@ -97,7 +122,7 @@ describe PostReceive do
before do before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) allow(Gitlab::GlRepository).to receive(:parse).and_return([project, project, Gitlab::GlRepository::PROJECT])
end end
shared_examples 'updating remote mirrors' do shared_examples 'updating remote mirrors' do
...@@ -176,7 +201,7 @@ describe PostReceive do ...@@ -176,7 +201,7 @@ describe PostReceive do
end end
before do before do
expect(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) expect(Gitlab::GlRepository).to receive(:parse).and_return([project, project, Gitlab::GlRepository::PROJECT])
end end
it 'does not expire branches cache' do it 'does not expire branches cache' do
...@@ -256,7 +281,7 @@ describe PostReceive do ...@@ -256,7 +281,7 @@ describe PostReceive do
before do before do
# Need to mock here so we can expect calls on project # Need to mock here so we can expect calls on project
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::WIKI]) allow(Gitlab::GlRepository).to receive(:parse).and_return([project, project, Gitlab::GlRepository::WIKI])
end end
it 'updates project activity' do it 'updates project activity' do
...@@ -333,4 +358,82 @@ describe PostReceive do ...@@ -333,4 +358,82 @@ describe PostReceive do
perform perform
end end
end end
describe '#process_snippet_changes' do
let(:gl_repository) { "snippet-#{snippet.id}" }
before do
# Need to mock here so we can expect calls on project
allow(Gitlab::GlRepository).to receive(:parse).and_return([snippet, snippet.project, Gitlab::GlRepository::SNIPPET])
end
shared_examples 'snippet changes actions' do
context 'unidentified user' do
let!(:key_id) { '' }
it 'returns false' do
expect(perform).to be false
end
end
context 'with changes' do
context 'branches' do
let(:changes) do
<<~EOF
123456 789012 refs/heads/tést1
123456 789012 refs/heads/tést2
EOF
end
it 'expires the branches cache' do
expect(snippet.repository).to receive(:expire_branches_cache).once
perform
end
it 'expires the status cache' do
expect(snippet.repository).to receive(:empty?).and_return(true)
expect(snippet.repository).to receive(:expire_status_cache)
perform
end
end
context 'tags' do
let(:changes) do
<<~EOF
654321 210987 refs/tags/tag1
654322 210986 refs/tags/tag2
654323 210985 refs/tags/tag3
EOF
end
it 'does not expire branches cache' do
expect(snippet.repository).not_to receive(:expire_branches_cache)
perform
end
it 'only invalidates tags once' do
expect(snippet.repository).to receive(:expire_caches_for_tags).once.and_call_original
expect(snippet.repository).to receive(:expire_tags_cache).once.and_call_original
perform
end
end
end
end
context 'with PersonalSnippet' do
let!(:snippet) { create(:personal_snippet, author: project.owner) }
it_behaves_like 'snippet changes actions'
end
context 'with ProjectSnippet' do
let!(:snippet) { create(:project_snippet, project: project, author: project.owner) }
it_behaves_like 'snippet changes actions'
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment