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
include KerberosSpnegoHelper
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 :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
......@@ -81,7 +81,7 @@ module Repositories
end
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
def render_missing_personal_access_token
......@@ -93,7 +93,7 @@ module Repositories
def repository
strong_memoize(:repository) do
repo_type.repository_for(project)
repo_type.repository_for(container)
end
end
......@@ -117,7 +117,8 @@ module Repositories
def http_download_allowed?
Gitlab::ProtocolAccess.allowed?('http') &&
download_request? &&
project && Guest.can?(:download_code, project)
container &&
Guest.can?(repo_type.guest_read_ability, container)
end
end
end
......
......@@ -84,10 +84,10 @@ module Repositories
end
def access
@access ||= access_klass.new(access_actor, project, 'http',
@access ||= access_klass.new(access_actor, container, 'http',
authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id],
project_path: project_path,
repository_path: repository_path,
redirected_path: redirected_path,
auth_result_type: auth_result_type)
end
......@@ -99,15 +99,18 @@ module Repositories
def access_check
access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
if repo_type.project? && !container
@project = @container = access.project
end
end
def access_klass
@access_klass ||= repo_type.access_checker_class
end
def project_path
@project_path ||= params[:repository_id].sub(/\.git$/, '')
def repository_path
@repository_path ||= params[:repository_id].sub(/\.git$/, '')
end
def log_user_activity
......
......@@ -160,6 +160,10 @@ class Snippet < ApplicationRecord
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
def self.find_by_id_and_project(id:, project:)
Snippet.find_by(id: id, project: project)
end
def initialize(attributes = {})
# 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
......
......@@ -4,10 +4,11 @@
#
# Used for scheduling related jobs after a push action has been performed
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
@repository = repository
@project = project
@params = params
end
......@@ -24,7 +25,7 @@ class PostReceiveService
mr_options = push_options.get(:merge_request)
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)
end
......@@ -46,8 +47,13 @@ class PostReceiveService
response
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')
return unless repository
unless repository.repo_type.project?
return push_options_warning('Push options are only supported for projects')
end
service = ::MergeRequests::PushOptionsHandlerService.new(
project, user, changes, push_options
......@@ -64,6 +70,8 @@ class PostReceiveService
end
def merge_request_urls
return [] unless repository&.repo_type&.project?
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
end
......@@ -9,9 +9,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
weight 5
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}\"")
return false
end
......@@ -20,12 +20,14 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
# Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's.
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?
process_wiki_changes(post_received)
process_wiki_changes(post_received, container)
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
# Other repos don't have hooks for now
end
......@@ -39,24 +41,50 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end
end
def process_project_changes(post_received)
def process_project_changes(post_received, project)
user = identify_user(post_received)
return false unless user
project = post_received.project
push_options = post_received.push_options
changes = post_received.changes
# We only need to expire certain caches once per push
expire_caches(post_received, post_received.project.repository)
enqueue_repository_cache_update(post_received)
expire_caches(post_received, project.repository)
enqueue_project_cache_update(post_received, project)
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)
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.
def expire_caches(post_received, repository)
repository.expire_status_cache if repository.empty?
......@@ -65,12 +93,12 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end
# 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 << :commit_count if post_received.includes_default_branch?
ProjectCacheWorker.perform_async(
post_received.project.id,
project.id,
[],
stats_to_invalidate,
true
......@@ -83,10 +111,9 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
Git::ProcessRefChangesService.new(project, user, params).execute
end
def update_remote_mirrors(post_received)
def update_remote_mirrors(post_received, project)
return unless post_received.includes_branches? || post_received.includes_tags?
project = post_received.project
return unless project.has_remote_mirror?
project.mark_stuck_remote_mirrors_as_failed!
......@@ -99,20 +126,6 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
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)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
......
---
title: Update git workflows and routes to allow snippets
merge_request: 21739
author:
type: added
......@@ -32,6 +32,14 @@ concern :lfsable do
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',
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
......
......@@ -79,7 +79,7 @@ module EE
end
def repository_full_path
File.join(params[:namespace_id], project_path)
File.join(params[:namespace_id], repository_path)
end
def decoded_authorization
......
......@@ -22,11 +22,11 @@ module EE
end
end
def process_wiki_changes(post_received)
def process_wiki_changes(post_received, project)
super
if ::Gitlab::Geo.primary?
::Geo::RepositoryUpdatedService.new(post_received.project.wiki.repository).execute
::Geo::RepositoryUpdatedService.new(project.wiki.repository).execute
end
end
......
......@@ -674,7 +674,7 @@ describe Gitlab::GitAccess do
protocol,
authentication_abilities: authentication_abilities,
namespace_path: namespace_path,
project_path: project_path,
repository_path: project_path,
redirected_path: redirected_path
)
end
......
......@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::GlRepository::RepoType do
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
it_behaves_like 'a repo type' 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.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
......@@ -6,7 +6,7 @@ describe Gitlab::GlRepository do
let_it_be(:project) { create(:project, :repository) }
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
......
......@@ -3,7 +3,7 @@
module API
module Helpers
module InternalHelpers
attr_reader :redirected_path
attr_reader :redirected_path, :container
delegate :wiki?, to: :repo_type
......@@ -22,10 +22,10 @@ module API
end
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,
namespace_path: namespace_path,
project_path: project_path,
repository_path: project_path,
redirected_path: redirected_path)
end
......@@ -80,7 +80,7 @@ module API
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project
@project, @repo_type, @redirected_path =
@container, @project, @repo_type, @redirected_path =
if params[:gl_repository]
Gitlab::GlRepository.parse(params[:gl_repository])
elsif params[:project]
......@@ -92,17 +92,17 @@ module API
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
repo_type.identifier_for_container(project)
repo_type.identifier_for_container(container)
end
def gl_project_path
def gl_repository_path
repository.full_path
end
# Return the repository depending on whether we want the wiki or the
# regular repository
def repository
@repository ||= repo_type.repository_for(project)
@repository ||= repo_type.repository_for(container)
end
# Return the Gitaly Address if it is enabled
......@@ -111,8 +111,8 @@ module API
{
repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage),
address: Gitlab::GitalyClient.address(container.repository_storage),
token: Gitlab::GitalyClient.token(container.repository_storage),
features: Feature::Gitaly.server_feature_flags
}
end
......
......@@ -67,7 +67,7 @@ module API
when ::Gitlab::GitAccessResult::Success
payload = {
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_username: actor.username,
git_config_options: [],
......@@ -216,7 +216,7 @@ module API
post '/post_receive' do
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)
......
......@@ -43,15 +43,15 @@ module Gitlab
PUSH_COMMANDS = %w{git-receive-pack}.freeze
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
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@authentication_abilities = Array(authentication_abilities)
@namespace_path = namespace_path || project&.namespace&.full_path
@project_path = project_path || project&.path
@repository_path = repository_path || project&.path
@redirected_path = redirected_path
@auth_result_type = auth_result_type
end
......@@ -224,7 +224,7 @@ module Gitlab
return unless user&.can?(:create_projects, namespace)
project_params = {
path: project_path,
path: repository_path,
namespace_id: namespace.id,
visibility_level: Gitlab::VisibilityLevel::PRIVATE
}
......
......@@ -3,10 +3,10 @@
module Gitlab
class GitPostReceive
include Gitlab::Identifier
attr_reader :project, :identifier, :changes, :push_options
attr_reader :container, :identifier, :changes, :push_options
def initialize(project, identifier, changes, push_options = {})
@project = project
def initialize(container, identifier, changes, push_options = {})
@container = container
@identifier = identifier
@changes = parse_changes(changes)
@push_options = push_options
......@@ -27,10 +27,10 @@ module Gitlab
def includes_default_branch?
# If the branch doesn't have a default branch yet, we presume the
# 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|
Gitlab::Git.branch_name(change[:ref]) == project.default_branch
Gitlab::Git.branch_name(change[:ref]) == container.default_branch
end
end
......
......@@ -7,19 +7,21 @@ module Gitlab
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccess,
repository_resolver: -> (project) { project.repository }
repository_resolver: -> (project) { project&.repository }
).freeze
WIKI = RepoType.new(
name: :wiki,
access_checker_class: Gitlab::GitAccessWiki,
repository_resolver: -> (project) { project.wiki.repository },
repository_resolver: -> (project) { project&.wiki&.repository },
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
name: :snippet,
access_checker_class: Gitlab::GitAccessSnippet,
repository_resolver: -> (snippet) { snippet.repository },
container_resolver: -> (id) { Snippet.find_by_id(id) }
repository_resolver: -> (snippet) { snippet&.repository },
container_resolver: -> (id) { Snippet.find_by_id(id) },
project_resolver: -> (snippet) { snippet&.project },
guest_read_ability: :read_snippet
).freeze
TYPES = {
......@@ -42,7 +44,7 @@ module Gitlab
container = type.fetch_container!(gl_repository)
[container, type]
[container, type.project_for(container), type]
end
def self.default_type
......
......@@ -7,6 +7,8 @@ module Gitlab
:access_checker_class,
:repository_resolver,
:container_resolver,
:project_resolver,
:guest_read_ability,
:suffix
def initialize(
......@@ -14,11 +16,15 @@ module Gitlab
access_checker_class:,
repository_resolver:,
container_resolver: default_container_resolver,
project_resolver: nil,
guest_read_ability: :download_code,
suffix: nil)
@name = name
@access_checker_class = access_checker_class
@repository_resolver = repository_resolver
@container_resolver = container_resolver
@project_resolver = project_resolver
@guest_read_ability = guest_read_ability
@suffix = suffix
end
......@@ -59,8 +65,18 @@ module Gitlab
repository_resolver.call(container)
end
def project_for(container)
return container unless project_resolver
project_resolver.call(container)
end
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
private
......
......@@ -237,8 +237,32 @@ module Gitlab
}x
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
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)
# Turns a multiline extended regexp into a single line one,
# because `rake routes` breaks on multiline regexes.
......
......@@ -19,22 +19,33 @@ module Gitlab
# Removing the suffix (.wiki, .design, ...) from the project path
full_path = repo_path.chomp(type.path_suffix)
project, was_redirected = find_project(full_path)
container, project, was_redirected = find_container(type, full_path)
redirected_path = repo_path if was_redirected
# If we found a matching project, then the type was matched, no need to
# continue looking.
return [project, type, redirected_path] if project
return [container, project, type, redirected_path] if container
end
# 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
# 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
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, redirected?(project, project_path)]
......@@ -43,6 +54,27 @@ module Gitlab
def self.redirected?(project, project_path)
project && project.full_path.casecmp(project_path) != 0
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
......
......@@ -24,7 +24,7 @@ module Gitlab
attrs = {
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,
ShowAllRefs: show_all_refs,
Repository: repository.gitaly_repository.to_h,
......
......@@ -6,16 +6,18 @@ describe Repositories::GitHttpController do
include GitHttpHelpers
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(:repository_id) { project.path + '.git' }
let(:project_params) do
let(:container_params) do
{
namespace_id: namespace_id,
repository_id: repository_id
}
end
let(:params) { project_params }
let(:params) { container_params }
describe 'HEAD #info_refs' do
it 'returns 403' do
......@@ -27,7 +29,7 @@ describe Repositories::GitHttpController do
shared_examples 'info_refs behavior' 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
stub_application_setting(enabled_git_access_protocol: 'ssh')
......@@ -41,8 +43,6 @@ describe Repositories::GitHttpController do
end
context 'with authorized user' do
let(:user) { project.owner }
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
......@@ -122,7 +122,7 @@ describe Repositories::GitHttpController do
end
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
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
......@@ -136,11 +136,41 @@ describe Repositories::GitHttpController do
end
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 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
let(:expected_object) { project }
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
......@@ -240,7 +240,7 @@ describe Gitlab::GitAccess do
let(:access) do
described_class.new(actor, nil,
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)
end
......@@ -259,7 +259,7 @@ describe Gitlab::GitAccess do
let(:access) do
described_class.new(actor, nil,
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)
end
......@@ -453,7 +453,7 @@ describe Gitlab::GitAccess do
let(:access) do
described_class.new(actor, project,
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)
end
......@@ -598,7 +598,7 @@ describe Gitlab::GitAccess do
let(:public_project) { create(:project, :public, :repository) }
let(:project_path) { public_project.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
it 'give access to download code' do
......@@ -1203,7 +1203,7 @@ describe Gitlab::GitAccess do
def access
described_class.new(actor, project, protocol,
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)
end
......
......@@ -5,46 +5,62 @@ describe Gitlab::GlRepository::RepoType do
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) }
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
it_behaves_like 'a repo type' do
let(:expected_identifier) { "project-#{project.id}" }
let(:expected_id) { project.id.to_s }
let(:expected_identifier) { "project-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_repository) { project.repository }
let(:expected_container) { project }
let(:expected_repository) { expected_container.repository }
end
it 'knows its type' do
expect(described_class).not_to be_wiki
expect(described_class).to be_project
expect(described_class).not_to be_snippet
aggregate_failures do
expect(described_class).not_to be_wiki
expect(described_class).to be_project
expect(described_class).not_to be_snippet
end
end
it 'checks if repository path is valid' do
expect(described_class.valid?(project.repository.full_path)).to be_truthy
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy
aggregate_failures do
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
describe Gitlab::GlRepository::WIKI do
it_behaves_like 'a repo type' do
let(:expected_identifier) { "wiki-#{project.id}" }
let(:expected_id) { project.id.to_s }
let(:expected_identifier) { "wiki-#{expected_id}" }
let(:expected_suffix) { '.wiki' }
let(:expected_repository) { project.wiki.repository }
let(:expected_container) { project }
let(:expected_repository) { expected_container.wiki.repository }
end
it 'knows its type' do
expect(described_class).to be_wiki
expect(described_class).not_to be_project
expect(described_class).not_to be_snippet
aggregate_failures do
expect(described_class).to be_wiki
expect(described_class).not_to be_project
expect(described_class).not_to be_snippet
end
end
it 'checks if repository path is valid' do
expect(described_class.valid?(project.repository.full_path)).to be_falsey
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy
aggregate_failures do
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
......@@ -59,9 +75,20 @@ describe Gitlab::GlRepository::RepoType do
end
it 'knows its type' do
expect(described_class).to be_snippet
expect(described_class).not_to be_wiki
expect(described_class).not_to be_project
aggregate_failures do
expect(described_class).to be_snippet
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
......@@ -75,9 +102,20 @@ describe Gitlab::GlRepository::RepoType do
end
it 'knows its type' do
expect(described_class).to be_snippet
expect(described_class).not_to be_wiki
expect(described_class).not_to be_project
aggregate_failures do
expect(described_class).to be_snippet
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
......
......@@ -5,13 +5,18 @@ require 'spec_helper'
describe ::Gitlab::GlRepository do
describe '.parse' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:snippet) { create(:personal_snippet) }
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
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
it 'throws an argument error on an invalid gl_repository type' do
......
......@@ -411,4 +411,37 @@ describe Gitlab::PathRegex do
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
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
......@@ -3,60 +3,72 @@
require 'spec_helper'
describe ::Gitlab::RepoPath do
describe '.parse' do
let_it_be(:project) { create(:project, :repository) }
include Gitlab::Routing
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
it 'parses a full repository path' do
expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil])
it 'parses a full repository project path' do
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
it 'parses a full wiki path' do
expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil])
it 'parses a project snippet repository path' do
expect(described_class.parse("#{project.full_path}/snippets/#{project_snippet.id}")).to eq([project_snippet, project, Gitlab::GlRepository::SNIPPET, nil])
end
end
context 'a relative 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
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
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
context 'of a redirected project' do
let(:redirect) { project.route.create_redirect('foo/bar') }
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
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
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
it "returns the default type for non existent paths" do
_project, type, _redirected = described_class.parse("path/non-existent.git")
expect(type).to eq(Gitlab::GlRepository.default_type)
it 'returns the default type for non existent paths' do
expect(described_class.parse('path/non-existent.git')).to eq([nil, nil, Gitlab::GlRepository.default_type, nil])
end
end
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 the cases match' do
it 'returns the project and false' do
......@@ -81,4 +93,34 @@ describe ::Gitlab::RepoPath do
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
......@@ -12,19 +12,44 @@ describe Gitlab::RepositoryCache do
describe '#cache_key' do
subject { cache.cache_key(:foo) }
it 'includes the namespace' do
expect(subject).to eq "foo:#{namespace}"
shared_examples 'cache_key examples' do
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
context 'with a given namespace' do
let(:extra_namespace) { 'my:data' }
let(:cache) do
described_class.new(repository, extra_namespace: extra_namespace,
backend: backend)
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
describe 'project snippet repository' do
let_it_be(:project_snippet) { create(:project_snippet, project: project) }
it 'includes the full namespace' do
expect(subject).to eq "foo:#{namespace}:#{extra_namespace}"
it_behaves_like 'cache_key examples' do
let(:repository) { project_snippet.repository }
end
end
end
......
......@@ -11,16 +11,41 @@ describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
describe '#cache_key' do
subject { cache.cache_key(:foo) }
it 'includes the namespace' do
is_expected.to eq("foo:#{namespace}:set")
shared_examples 'cache_key examples' do
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
context 'with a given namespace' do
let(:extra_namespace) { 'my:data' }
let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
describe 'project snippet repository' do
let_it_be(:project_snippet) { create(:project_snippet, project: project) }
it 'includes the full namespace' do
is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set")
it_behaves_like 'cache_key examples' do
let(:repository) { project_snippet.repository }
end
end
end
......
This diff is collapsed.
This diff is collapsed.
......@@ -34,6 +34,31 @@ describe PostReceive do
expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
expect(perform).to be(false)
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
describe "#process_project_changes" do
......@@ -44,7 +69,7 @@ describe PostReceive do
before do
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
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
it 'expire the status cache' do
......@@ -97,7 +122,7 @@ describe PostReceive do
before do
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
shared_examples 'updating remote mirrors' do
......@@ -176,7 +201,7 @@ describe PostReceive do
end
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
it 'does not expire branches cache' do
......@@ -256,7 +281,7 @@ describe PostReceive do
before do
# 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
it 'updates project activity' do
......@@ -333,4 +358,82 @@ describe PostReceive do
perform
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
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