Commit e8ca2229 authored by Francisco Javier López's avatar Francisco Javier López Committed by Nick Thomas

Add snippet git routes

In this commit we add the snippet git commit
routes that will allow us to perform git
operations on snippets.

It also refactors the Repositories controllers
to allow working with containers and making
them project agnostic.
parent f1e37610
......@@ -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