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
......
......@@ -10,6 +10,9 @@ describe API::Internal::Base do
let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user, project: project) }
describe "GET /internal/check" do
it do
expect_any_instance_of(Redis).to receive(:ping).and_return('PONG')
......@@ -312,6 +315,54 @@ describe API::Internal::Base do
end
end
context 'git push with personal snippet' do
it 'responds with success' do
push(key, personal_snippet)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(personal_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}")
expect(user.reload.last_activity_on).to be_nil
end
end
context 'git pull with personal snippet' do
it 'responds with success' do
pull(key, personal_snippet)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(personal_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
context 'git push with project snippet' do
it 'responds with success' do
push(key, project_snippet)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(project_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}")
expect(user.reload.last_activity_on).to be_nil
end
end
context 'git pull with project snippet' do
it 'responds with success' do
pull(key, project_snippet)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(project_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
context "git pull" do
before do
allow(Feature).to receive(:persisted_names).and_return(%w[gitaly_mep_mep])
......@@ -393,10 +444,28 @@ describe API::Internal::Base do
end
end
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username, project: project.full_path } }
context 'with Project' do
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username, project: project.full_path } }
subject { push(key, project) }
end
end
context 'with PersonalSnippet' do
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username } }
subject { push(key, personal_snippet) }
end
end
subject { push(key, project) }
context 'with ProjectSnippet' do
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username, project: project_snippet.project.full_path } }
subject { push(key, project_snippet) }
end
end
end
......@@ -450,7 +519,7 @@ describe API::Internal::Base do
{
authentication_abilities: [:read_project, :download_code, :push_code],
namespace_path: project.namespace.path,
project_path: project.path,
repository_path: project.path,
redirected_path: nil
}
).and_return(access_checker)
......@@ -835,22 +904,60 @@ describe API::Internal::Base do
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
end
it 'executes PostReceiveService' do
message = <<~MESSAGE.strip
To create a merge request for #{branch_name}, visit:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}
MESSAGE
context 'with Project' do
it 'executes PostReceiveService' do
message = <<~MESSAGE.strip
To create a merge request for #{branch_name}, visit:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}
MESSAGE
subject
subject
expect(json_response).to eq({
'messages' => [{ 'message' => message, 'type' => 'basic' }],
'reference_counter_decreased' => true
})
end
expect(json_response).to eq({
'messages' => [{ 'message' => message, 'type' => 'basic' }],
'reference_counter_decreased' => true
})
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: user.username, project: project.full_path } }
end
end
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: user.username, project: project.full_path } }
context 'with PersonalSnippet' do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
it 'executes PostReceiveService' do
subject
expect(json_response).to eq({
'messages' => [],
'reference_counter_decreased' => true
})
end
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username } }
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
end
end
context 'with ProjectSnippet' do
let(:gl_repository) { "snippet-#{project_snippet.id}" }
it 'executes PostReceiveService' do
subject
expect(json_response).to eq({
'messages' => [],
'reference_counter_decreased' => true
})
end
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: key.user.username, project: project_snippet.project.full_path } }
let(:gl_repository) { "snippet-#{project_snippet.id}" }
end
end
context 'with an orphaned write deploy key' do
......@@ -866,16 +973,32 @@ describe API::Internal::Base do
end
context 'when project is nil' do
let(:gl_repository) { 'project-foo' }
context 'with Project' do
let(:gl_repository) { 'project-foo' }
it 'does not try to notify that project moved' do
allow(Gitlab::GlRepository).to receive(:parse).and_return([nil, Gitlab::GlRepository::PROJECT])
it 'does not try to notify that project moved' do
allow(Gitlab::GlRepository).to receive(:parse).and_return([nil, nil, Gitlab::GlRepository::PROJECT])
expect(Gitlab::Checks::ProjectMoved).not_to receive(:fetch_message)
expect(Gitlab::Checks::ProjectMoved).not_to receive(:fetch_message)
subject
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with PersonalSnippet' do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
it 'does not try to notify that project moved' do
allow(Gitlab::GlRepository).to receive(:parse).and_return([personal_snippet, nil, Gitlab::GlRepository::PROJECT])
expect(Gitlab::Checks::ProjectMoved).not_to receive(:fetch_message)
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
......@@ -896,24 +1019,37 @@ describe API::Internal::Base do
end
end
def gl_repository_for(project_or_wiki)
case project_or_wiki
def gl_repository_for(container)
case container
when ProjectWiki
Gitlab::GlRepository::WIKI.identifier_for_container(project_or_wiki.project)
Gitlab::GlRepository::WIKI.identifier_for_container(container.project)
when Project
Gitlab::GlRepository::PROJECT.identifier_for_container(project_or_wiki)
Gitlab::GlRepository::PROJECT.identifier_for_container(container)
when Snippet
Gitlab::GlRepository::SNIPPET.identifier_for_container(container)
else
nil
end
end
def pull(key, project, protocol = 'ssh')
def full_path_for(container)
case container
when PersonalSnippet
"snippets/#{container.id}"
when ProjectSnippet
"#{container.project.full_path}/snippets/#{container.id}"
else
container.full_path
end
end
def pull(key, container, protocol = 'ssh')
post(
api("/internal/allowed"),
params: {
key_id: key.id,
project: project.full_path,
gl_repository: gl_repository_for(project),
project: full_path_for(container),
gl_repository: gl_repository_for(container),
action: 'git-upload-pack',
secret_token: secret_token,
protocol: protocol
......@@ -921,12 +1057,12 @@ describe API::Internal::Base do
)
end
def push(key, project, protocol = 'ssh', env: nil)
def push(key, container, protocol = 'ssh', env: nil)
params = {
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
project: project.full_path,
gl_repository: gl_repository_for(project),
project: full_path_for(container),
gl_repository: gl_repository_for(container),
action: 'git-receive-pack',
secret_token: secret_token,
protocol: protocol,
......@@ -939,14 +1075,14 @@ describe API::Internal::Base do
)
end
def archive(key, project)
def archive(key, container)
post(
api("/internal/allowed"),
params: {
ref: 'master',
key_id: key.id,
project: project.full_path,
gl_repository: gl_repository_for(project),
project: full_path_for(container),
gl_repository: gl_repository_for(container),
action: 'git-upload-archive',
secret_token: secret_token,
protocol: 'ssh'
......
......@@ -5,8 +5,10 @@ require 'spec_helper'
describe PostReceiveService do
include Gitlab::Routing
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project, author: user) }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
let(:identifier) { 'key-123' }
let(:gl_repository) { "project-#{project.id}" }
......@@ -14,6 +16,7 @@ describe PostReceiveService do
let(:secret_token) { Gitlab::Shell.secret_token }
let(:reference_counter) { double('ReferenceCounter') }
let(:push_options) { ['ci.skip', 'another push option'] }
let(:repository) { project.repository }
let(:changes) do
"#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
......@@ -29,104 +32,173 @@ describe PostReceiveService do
}
end
let(:response) { PostReceiveService.new(user, project, params).execute }
let(:service) { described_class.new(user, repository, project, params) }
let(:response) { service.execute }
subject { response.messages.as_json }
it 'enqueues a PostReceive worker job' do
expect(PostReceive).to receive(:perform_async)
.with(gl_repository, identifier, changes, { ci: { skip: true } })
context 'when project is nil' do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
let(:project) { nil }
let(:repository) { personal_snippet.repository }
subject
it 'does not return error' do
expect(subject).to be_empty
end
end
it 'decreases the reference counter and returns the result' do
expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository)
.and_return(reference_counter)
expect(reference_counter).to receive(:decrease).and_return(true)
context 'when repository is nil' do
let(:repository) { nil }
expect(response.reference_counter_decreased).to be(true)
it 'does not return error' do
expect(subject).to be_empty
end
end
it 'returns link to create new merge request' do
message = <<~MESSAGE.strip
To create a merge request for #{branch_name}, visit:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}
MESSAGE
context 'when both repository and project are nil' do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
let(:project) { nil }
let(:repository) { nil }
expect(subject).to include(build_basic_message(message))
it 'does not return error' do
expect(subject).to be_empty
end
end
it 'returns the link to an existing merge request when it exists' do
merge_request = create(:merge_request, source_project: project, source_branch: branch_name, target_branch: 'master')
message = <<~MESSAGE.strip
View merge request for feature:
#{project_merge_request_url(project, merge_request)}
MESSAGE
shared_examples 'post_receive_service actions' do
it 'enqueues a PostReceive worker job' do
expect(PostReceive).to receive(:perform_async)
.with(gl_repository, identifier, changes, { ci: { skip: true } })
expect(subject).to include(build_basic_message(message))
end
subject
end
context 'when printing_merge_request_link_enabled is false' do
let(:project) { create(:project, printing_merge_request_link_enabled: false) }
it 'decreases the reference counter and returns the result' do
expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository)
.and_return(reference_counter)
expect(reference_counter).to receive(:decrease).and_return(true)
it 'returns no merge request messages' do
expect(subject).to be_blank
expect(response.reference_counter_decreased).to be(true)
end
end
it 'does not invoke MergeRequests::PushOptionsHandlerService' do
expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
context 'with Project' do
it_behaves_like 'post_receive_service actions'
subject
end
it 'returns link to create new merge request' do
message = <<~MESSAGE.strip
To create a merge request for #{branch_name}, visit:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}
MESSAGE
context 'when there are merge_request push options' do
let(:params) { super().merge(push_options: ['merge_request.create']) }
expect(subject).to include(build_basic_message(message))
end
it 'returns the link to an existing merge request when it exists' do
merge_request = create(:merge_request, source_project: project, source_branch: branch_name, target_branch: 'master')
message = <<~MESSAGE.strip
View merge request for feature:
#{project_merge_request_url(project, merge_request)}
MESSAGE
before do
project.add_developer(user)
expect(subject).to include(build_basic_message(message))
end
it 'invokes MergeRequests::PushOptionsHandlerService' do
expect(MergeRequests::PushOptionsHandlerService).to receive(:new).and_call_original
context 'when printing_merge_request_link_enabled is false' do
let(:project) { create(:project, printing_merge_request_link_enabled: false) }
subject
it 'returns no merge request messages' do
expect(subject).to be_blank
end
end
it 'creates a new merge request' do
expect { Sidekiq::Testing.fake! { subject } }.to change(MergeRequest, :count).by(1)
it 'does not invoke MergeRequests::PushOptionsHandlerService' do
expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
subject
end
it 'links to the newly created merge request' do
message = <<~MESSAGE.strip
View merge request for #{branch_name}:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/1
MESSAGE
context 'when there are merge_request push options' do
let(:params) { super().merge(push_options: ['merge_request.create']) }
expect(subject).to include(build_basic_message(message))
before do
project.add_developer(user)
end
it 'invokes MergeRequests::PushOptionsHandlerService' do
expect(MergeRequests::PushOptionsHandlerService).to receive(:new).and_call_original
subject
end
it 'creates a new merge request' do
expect { Sidekiq::Testing.fake! { subject } }.to change(MergeRequest, :count).by(1)
end
it 'links to the newly created merge request' do
message = <<~MESSAGE.strip
View merge request for #{branch_name}:
http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/1
MESSAGE
expect(subject).to include(build_basic_message(message))
end
it 'adds errors on the service instance to warnings' do
expect_any_instance_of(
MergeRequests::PushOptionsHandlerService
).to receive(:errors).at_least(:once).and_return(['my error'])
message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error"
expect(subject).to include(build_alert_message(message))
end
it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
invalid_merge_request = MergeRequest.new
invalid_merge_request.errors.add(:base, 'my error')
message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error"
expect_any_instance_of(
MergeRequests::CreateService
).to receive(:execute).and_return(invalid_merge_request)
expect(subject).to include(build_alert_message(message))
end
end
end
context 'with PersonalSnippet' do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
let(:repository) { personal_snippet.repository }
it 'adds errors on the service instance to warnings' do
expect_any_instance_of(
MergeRequests::PushOptionsHandlerService
).to receive(:errors).at_least(:once).and_return(['my error'])
it_behaves_like 'post_receive_service actions'
message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error"
it 'does not return link to create new merge request' do
expect(subject).to be_empty
end
it 'does not return the link to an existing merge request when it exists' do
create(:merge_request, source_project: project, source_branch: branch_name, target_branch: 'master')
expect(subject).to include(build_alert_message(message))
expect(subject).to be_empty
end
end
it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
invalid_merge_request = MergeRequest.new
invalid_merge_request.errors.add(:base, 'my error')
message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error"
context 'with ProjectSnippet' do
let(:gl_repository) { "snippet-#{project_snippet.id}" }
let(:repository) { project_snippet.repository }
expect_any_instance_of(
MergeRequests::CreateService
).to receive(:execute).and_return(invalid_merge_request)
it_behaves_like 'post_receive_service actions'
expect(subject).to include(build_alert_message(message))
it 'does not return link to create new merge request' do
expect(subject).to be_empty
end
it 'does not return the link to an existing merge request when it exists' do
create(:merge_request, source_project: project, source_branch: branch_name, target_branch: 'master')
expect(subject).to be_empty
end
end
......@@ -178,6 +250,50 @@ describe PostReceiveService do
end
end
describe '#process_mr_push_options' do
context 'when repository belongs to a snippet' do
context 'with PersonalSnippet' do
let(:repository) { personal_snippet.repository }
it 'returns an error message' do
result = service.process_mr_push_options(push_options, changes)
expect(result).to match('Push options are only supported for projects')
end
end
context 'with ProjectSnippet' do
let(:repository) { project_snippet.repository }
it 'returns an error message' do
result = service.process_mr_push_options(push_options, changes)
expect(result).to match('Push options are only supported for projects')
end
end
end
end
describe '#merge_request_urls' do
context 'when repository belongs to a snippet' do
context 'with PersonalSnippet' do
let(:repository) { personal_snippet.repository }
it 'returns an empty array' do
expect(service.merge_request_urls).to be_empty
end
end
context 'with ProjectSnippet' do
let(:repository) { project_snippet.repository }
it 'returns an empty array' do
expect(service.merge_request_urls).to be_empty
end
end
end
end
def build_alert_message(message)
{ 'type' => 'alert', 'message' => message }
end
......
......@@ -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