Add snippet repository model

We need to create the repository model
for snippets. It will be the same for both
project and personal ones.

This commit also refactors some code in
order to reuse it in projects and snippets
parent 66acda99
...@@ -244,6 +244,8 @@ class Commit ...@@ -244,6 +244,8 @@ class Commit
# Discover issues should be closed when this commit is pushed to a project's # Discover issues should be closed when this commit is pushed to a project's
# default branch. # default branch.
def closes_issues(current_user = self.committer) def closes_issues(current_user = self.committer)
return unless repository.repo_type.project?
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end end
...@@ -297,7 +299,11 @@ class Commit ...@@ -297,7 +299,11 @@ class Commit
end end
def merge_requests def merge_requests
@merge_requests ||= project&.merge_requests&.by_commit_sha(sha) strong_memoize(:merge_requests) do
next MergeRequest.none unless repository.repo_type.project? && project
project.merge_requests.by_commit_sha(sha)
end
end end
def method_missing(method, *args, &block) def method_missing(method, *args, &block)
...@@ -507,7 +513,7 @@ class Commit ...@@ -507,7 +513,7 @@ class Commit
end end
def commit_reference(from, referable_commit_id, full: false) def commit_reference(from, referable_commit_id, full: false)
base = project&.to_reference_base(from, full: full) base = container.to_reference_base(from, full: full)
if base.present? if base.present?
"#{base}#{self.class.reference_prefix}#{referable_commit_id}" "#{base}#{self.class.reference_prefix}#{referable_commit_id}"
......
# frozen_string_literal: true # frozen_string_literal: true
# This concern is created to handle repository actions.
# It should be include inside any object capable
# of directly having a repository, like project or snippet.
#
# It also includes `Referable`, therefore the method
# `to_reference` should be overriden in case the object
# needs any special behavior.
module HasRepository module HasRepository
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include AfterCommitQueue include AfterCommitQueue
include Referable
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
delegate :base_dir, :disk_path, to: :storage delegate :base_dir, :disk_path, to: :storage
......
...@@ -2,4 +2,8 @@ ...@@ -2,4 +2,8 @@
class PersonalSnippet < Snippet class PersonalSnippet < Snippet
include WithUploads include WithUploads
def web_url(only_path: nil)
Gitlab::Routing.url_helpers.snippet_url(self, only_path: only_path)
end
end end
...@@ -9,7 +9,6 @@ class Project < ApplicationRecord ...@@ -9,7 +9,6 @@ class Project < ApplicationRecord
include AccessRequestable include AccessRequestable
include Avatarable include Avatarable
include CacheMarkdownField include CacheMarkdownField
include Referable
include Sortable include Sortable
include AfterCommitQueue include AfterCommitQueue
include CaseSensitivity include CaseSensitivity
......
...@@ -5,4 +5,8 @@ class ProjectSnippet < Snippet ...@@ -5,4 +5,8 @@ class ProjectSnippet < Snippet
validates :project, presence: true validates :project, presence: true
validates :secret, inclusion: { in: [false] } validates :secret, inclusion: { in: [false] }
def web_url(only_path: nil)
Gitlab::Routing.url_helpers.project_snippet_url(project, self, only_path: only_path)
end
end end
...@@ -1143,8 +1143,12 @@ class Repository ...@@ -1143,8 +1143,12 @@ class Repository
end end
def project def project
if repo_type.snippet?
container.project
else
container container
end end
end
private private
...@@ -1157,7 +1161,7 @@ class Repository ...@@ -1157,7 +1161,7 @@ class Repository
Gitlab::Git::Commit.find(raw_repository, oid_or_ref) Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end end
::Commit.new(commit, project) if commit ::Commit.new(commit, container) if commit
end end
def cache def cache
......
...@@ -6,7 +6,6 @@ class Snippet < ApplicationRecord ...@@ -6,7 +6,6 @@ class Snippet < ApplicationRecord
include CacheMarkdownField include CacheMarkdownField
include Noteable include Noteable
include Participable include Participable
include Referable
include Sortable include Sortable
include Awardable include Awardable
include Mentionable include Mentionable
...@@ -15,10 +14,11 @@ class Snippet < ApplicationRecord ...@@ -15,10 +14,11 @@ class Snippet < ApplicationRecord
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include FromUnion include FromUnion
include IgnorableColumns include IgnorableColumns
include HasRepository
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
ignore_column :storage_version, remove_with: '12.9', remove_after: '2020-03-22' ignore_column :storage_version, remove_with: '12.9', remove_after: '2020-03-22'
ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-04-22'
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description cache_markdown_field :description
...@@ -42,6 +42,7 @@ class Snippet < ApplicationRecord ...@@ -42,6 +42,7 @@ class Snippet < ApplicationRecord
has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "SnippetUserMention" has_many :user_mentions, class_name: "SnippetUserMention"
has_one :snippet_repository, inverse_of: :snippet
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
...@@ -254,6 +255,47 @@ class Snippet < ApplicationRecord ...@@ -254,6 +255,47 @@ class Snippet < ApplicationRecord
super super
end end
def repository
@repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET)
end
def storage
@storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX)
end
# This is the full_path used to identify the
# the snippet repository. It will be used mostly
# for logging purposes.
def full_path
return unless persisted?
@full_path ||= begin
components = []
components << project.full_path if project_id?
components << '@snippets'
components << self.id
components.join('/')
end
end
def repository_storage
snippet_repository&.shard_name ||
Gitlab::CurrentSettings.pick_repository_storage
end
def create_repository
return if repository_exists?
repository.create_if_not_exists
track_snippet_repository if repository_exists?
end
def track_snippet_repository
repository = snippet_repository || build_snippet_repository
repository.update!(shard_name: repository_storage, disk_path: disk_path)
end
class << self class << self
# Searches for snippets with a matching title or file name. # Searches for snippets with a matching title or file name.
# #
......
# frozen_string_literal: true
class SnippetRepository < ApplicationRecord
include Shardable
belongs_to :snippet, inverse_of: :snippet_repository
class << self
def find_snippet(disk_path)
find_by(disk_path: disk_path)&.snippet
end
end
end
...@@ -2,14 +2,15 @@ ...@@ -2,14 +2,15 @@
module Storage module Storage
class Hashed class Hashed
attr_accessor :project attr_accessor :container
delegate :gitlab_shell, :repository_storage, to: :project delegate :gitlab_shell, :repository_storage, to: :container
REPOSITORY_PATH_PREFIX = '@hashed' REPOSITORY_PATH_PREFIX = '@hashed'
SNIPPET_REPOSITORY_PATH_PREFIX = '@snippets'
POOL_PATH_PREFIX = '@pools' POOL_PATH_PREFIX = '@pools'
def initialize(project, prefix: REPOSITORY_PATH_PREFIX) def initialize(container, prefix: REPOSITORY_PATH_PREFIX)
@project = project @container = container
@prefix = prefix @prefix = prefix
end end
...@@ -20,9 +21,10 @@ module Storage ...@@ -20,9 +21,10 @@ module Storage
"#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash "#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
end end
# Disk path is used to build repository and project's wiki path on disk # Disk path is used to build repository path on disk
# #
# @return [String] combination of base_dir and the repository own name without `.git` or `.wiki.git` extensions # @return [String] combination of base_dir and the repository own name
# without `.git`, `.wiki.git`, or any other extension
def disk_path def disk_path
"#{base_dir}/#{disk_hash}" if disk_hash "#{base_dir}/#{disk_hash}" if disk_hash
end end
...@@ -33,10 +35,10 @@ module Storage ...@@ -33,10 +35,10 @@ module Storage
private private
# Generates the hash for the project path and name on disk # Generates the hash for the repository path and name on disk
# If you need to refer to the repository on disk, use the `#disk_path` # If you need to refer to the repository on disk, use the `#disk_path`
def disk_hash def disk_hash
@disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id @disk_hash ||= Digest::SHA2.hexdigest(container.id.to_s) if container.id
end end
end end
end end
---
title: Create snippet repository model
merge_request: 23796
author:
type: added
# frozen_string_literal: true
class CreateSnippetRepositoryTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :snippet_repositories, id: false, primary_key: :snippet_id do |t|
t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
t.references :snippet, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
t.string :disk_path, limit: 80, null: false, index: { unique: true }
end
end
end
...@@ -3892,6 +3892,13 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do ...@@ -3892,6 +3892,13 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do
t.index ["user_id"], name: "index_smartcard_identities_on_user_id" t.index ["user_id"], name: "index_smartcard_identities_on_user_id"
end end
create_table "snippet_repositories", primary_key: "snippet_id", id: :bigint, default: nil, force: :cascade do |t|
t.bigint "shard_id", null: false
t.string "disk_path", limit: 80, null: false
t.index ["disk_path"], name: "index_snippet_repositories_on_disk_path", unique: true
t.index ["shard_id"], name: "index_snippet_repositories_on_shard_id"
end
create_table "snippet_user_mentions", force: :cascade do |t| create_table "snippet_user_mentions", force: :cascade do |t|
t.integer "snippet_id", null: false t.integer "snippet_id", null: false
t.integer "note_id" t.integer "note_id"
...@@ -4963,6 +4970,8 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do ...@@ -4963,6 +4970,8 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "smartcard_identities", "users", on_delete: :cascade add_foreign_key "smartcard_identities", "users", on_delete: :cascade
add_foreign_key "snippet_repositories", "shards", on_delete: :restrict
add_foreign_key "snippet_repositories", "snippets", on_delete: :cascade
add_foreign_key "snippet_user_mentions", "notes", on_delete: :cascade add_foreign_key "snippet_user_mentions", "notes", on_delete: :cascade
add_foreign_key "snippet_user_mentions", "snippets", on_delete: :cascade add_foreign_key "snippet_user_mentions", "snippets", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
......
...@@ -17,6 +17,7 @@ describe Gitlab::GlRepository::RepoType do ...@@ -17,6 +17,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class).to be_design expect(described_class).to be_design
expect(described_class).not_to be_project expect(described_class).not_to be_project
expect(described_class).not_to be_wiki expect(described_class).not_to be_wiki
expect(described_class).not_to be_snippet
end end
it 'checks if repository path is valid' do it 'checks if repository path is valid' do
......
# frozen_string_literal: true
module Gitlab
class GitAccessSnippet < GitAccess
ERROR_MESSAGES = {
snippet_not_found: 'The snippet you were looking for could not be found.',
repository_not_found: 'The snippet repository you were looking for could not be found.'
}.freeze
attr_reader :snippet
def initialize(actor, snippet, protocol, **kwargs)
@snippet = snippet
super(actor, project, protocol, **kwargs)
end
def check(cmd, _changes)
unless Feature.enabled?(:version_snippets, user)
raise NotFoundError, ERROR_MESSAGES[:snippet_not_found]
end
check_snippet_accessibility!
success_result(cmd)
end
def project
snippet&.project
end
private
def repository
snippet&.repository
end
def check_snippet_accessibility!
if snippet.blank?
raise NotFoundError, ERROR_MESSAGES[:snippet_not_found]
end
unless repository&.exists?
raise NotFoundError, ERROR_MESSAGES[:repository_not_found]
end
end
end
end
...@@ -15,10 +15,17 @@ module Gitlab ...@@ -15,10 +15,17 @@ module Gitlab
repository_resolver: -> (project) { project.wiki.repository }, repository_resolver: -> (project) { project.wiki.repository },
suffix: :wiki suffix: :wiki
).freeze ).freeze
SNIPPET = RepoType.new(
name: :snippet,
access_checker_class: Gitlab::GitAccessSnippet,
repository_resolver: -> (snippet) { snippet.repository },
container_resolver: -> (id) { Snippet.find_by_id(id) }
).freeze
TYPES = { TYPES = {
PROJECT.name.to_s => PROJECT, PROJECT.name.to_s => PROJECT,
WIKI.name.to_s => WIKI WIKI.name.to_s => WIKI,
SNIPPET.name.to_s => SNIPPET
}.freeze }.freeze
def self.types def self.types
......
...@@ -47,6 +47,10 @@ module Gitlab ...@@ -47,6 +47,10 @@ module Gitlab
self == PROJECT self == PROJECT
end end
def snippet?
self == SNIPPET
end
def path_suffix def path_suffix
suffix ? ".#{suffix}" : '' suffix ? ".#{suffix}" : ''
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :snippet_repository do
snippet
after(:build) do |snippet_repository, _|
snippet_repository.shard_name = snippet_repository.snippet.repository_storage
snippet_repository.disk_path = snippet_repository.snippet.disk_path
end
end
end
...@@ -20,6 +20,21 @@ FactoryBot.define do ...@@ -20,6 +20,21 @@ FactoryBot.define do
trait :private do trait :private do
visibility_level { Snippet::PRIVATE } visibility_level { Snippet::PRIVATE }
end end
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
after :create do |snippet|
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
end
end
trait :empty_repo do
after(:create) do |snippet|
raise "Failed to create repository!" unless snippet.repository.create_if_not_exists
end
end
end end
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GitAccessSnippet do
include GitHelpers
let_it_be(:user) { create(:user) }
let_it_be(:personal_snippet) { create(:personal_snippet, :private, :repository) }
let(:protocol) { 'ssh' }
let(:changes) { Gitlab::GitAccess::ANY }
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
let(:snippet) { personal_snippet }
let(:actor) { personal_snippet.author }
describe 'when feature flag :version_snippets is enabled' do
it 'allows push and pull access' do
aggregate_failures do
expect { pull_access_check }.not_to raise_error
expect { push_access_check }.not_to raise_error
end
end
end
describe 'when feature flag :version_snippets is disabled' do
before do
stub_feature_flags(version_snippets: false)
end
it 'does not allow push and pull access' do
aggregate_failures do
expect { push_access_check }.to raise_snippet_not_found
expect { pull_access_check }.to raise_snippet_not_found
end
end
end
describe '#check_snippet_accessibility!' do
context 'when the snippet exists' do
it 'allows push and pull access' do
aggregate_failures do
expect { pull_access_check }.not_to raise_error
expect { push_access_check }.not_to raise_error
end
end
end
context 'when the snippet is nil' do
let(:snippet) { nil }
it 'blocks push and pull with "not found"' do
aggregate_failures do
expect { pull_access_check }.to raise_snippet_not_found
expect { push_access_check }.to raise_snippet_not_found
end
end
end
context 'when the snippet does not have a repository' do
let(:snippet) { build_stubbed(:personal_snippet) }
it 'blocks push and pull with "not found"' do
aggregate_failures do
expect { pull_access_check }.to raise_snippet_not_found
expect { push_access_check }.to raise_snippet_not_found
end
end
end
end
private
def access
described_class.new(actor, snippet, protocol,
authentication_abilities: [],
namespace_path: nil, project_path: nil,
redirected_path: nil, auth_result_type: nil)
end
def raise_snippet_not_found
raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:snippet_not_found])
end
end
...@@ -3,6 +3,8 @@ require 'spec_helper' ...@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::GlRepository::RepoType do describe Gitlab::GlRepository::RepoType do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
describe Gitlab::GlRepository::PROJECT do describe Gitlab::GlRepository::PROJECT do
it_behaves_like 'a repo type' do it_behaves_like 'a repo type' do
...@@ -16,6 +18,7 @@ describe Gitlab::GlRepository::RepoType do ...@@ -16,6 +18,7 @@ describe Gitlab::GlRepository::RepoType do
it 'knows its type' do it 'knows its type' do
expect(described_class).not_to be_wiki expect(described_class).not_to be_wiki
expect(described_class).to be_project expect(described_class).to be_project
expect(described_class).not_to be_snippet
end end
it 'checks if repository path is valid' do it 'checks if repository path is valid' do
...@@ -36,6 +39,7 @@ describe Gitlab::GlRepository::RepoType do ...@@ -36,6 +39,7 @@ describe Gitlab::GlRepository::RepoType do
it 'knows its type' do it 'knows its type' do
expect(described_class).to be_wiki expect(described_class).to be_wiki
expect(described_class).not_to be_project expect(described_class).not_to be_project
expect(described_class).not_to be_snippet
end end
it 'checks if repository path is valid' do it 'checks if repository path is valid' do
...@@ -43,4 +47,38 @@ describe Gitlab::GlRepository::RepoType do ...@@ -43,4 +47,38 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy
end end
end end
describe Gitlab::GlRepository::SNIPPET do
context 'when PersonalSnippet' do
it_behaves_like 'a repo type' do
let(:expected_id) { personal_snippet.id.to_s }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_repository) { personal_snippet.repository }
let(:expected_container) { personal_snippet }
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
end
end
context 'when ProjectSnippet' do
it_behaves_like 'a repo type' do
let(:expected_id) { project_snippet.id.to_s }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_repository) { project_snippet.repository }
let(:expected_container) { project_snippet }
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
end
end
end
end end
...@@ -91,6 +91,7 @@ snippets: ...@@ -91,6 +91,7 @@ snippets:
- award_emoji - award_emoji
- user_agent_detail - user_agent_detail
- user_mentions - user_mentions
- snippet_repository
releases: releases:
- author - author
- project - project
......
This diff is collapsed.
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe Commit do describe Commit do
let(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :repository) }
let(:commit) { project.commit } let(:commit) { project.commit }
describe 'modules' do describe 'modules' do
...@@ -17,8 +19,7 @@ describe Commit do ...@@ -17,8 +19,7 @@ describe Commit do
end end
describe '.lazy' do describe '.lazy' do
let_it_be(:project) { create(:project, :repository) } shared_examples '.lazy checks' do
context 'when the commits are found' do context 'when the commits are found' do
let(:oids) do let(:oids) do
%w( %w(
...@@ -30,10 +31,10 @@ describe Commit do ...@@ -30,10 +31,10 @@ describe Commit do
) )
end end
subject { oids.map { |oid| described_class.lazy(project, oid) } } subject { oids.map { |oid| described_class.lazy(container, oid) } }
it 'batches requests for commits' do it 'batches requests for commits' do
expect(project.repository).to receive(:commits_by).once.and_call_original expect(container.repository).to receive(:commits_by).once.and_call_original
subject.first.title subject.first.title
subject.last.title subject.last.title
...@@ -56,13 +57,32 @@ describe Commit do ...@@ -56,13 +57,32 @@ describe Commit do
context 'when not found' do context 'when not found' do
it 'returns nil as commit' do it 'returns nil as commit' do
commit = described_class.lazy(project, 'deadbeef').__sync commit = described_class.lazy(container, 'deadbeef').__sync
expect(commit).to be_nil expect(commit).to be_nil
end end
end end
end end
context 'with project' do
let(:container) { project }
it_behaves_like '.lazy checks'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like '.lazy checks'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like '.lazy checks'
end
end
describe '#diff_refs' do describe '#diff_refs' do
it 'is equal to itself' do it 'is equal to itself' do
expect(commit.diff_refs).to eq(commit.diff_refs) expect(commit.diff_refs).to eq(commit.diff_refs)
...@@ -231,6 +251,7 @@ describe Commit do ...@@ -231,6 +251,7 @@ describe Commit do
end end
describe '#to_reference' do describe '#to_reference' do
context 'with project' do
let(:project) { create(:project, :repository, path: 'sample-project') } let(:project) { create(:project, :repository, path: 'sample-project') }
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
...@@ -243,6 +264,33 @@ describe Commit do ...@@ -243,6 +264,33 @@ describe Commit do
end end
end end
context 'with personal snippet' do
let(:commit) { personal_snippet.commit }
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq "$#{personal_snippet.id}@#{commit.id}"
end
it 'supports a cross-snippet reference' do
another_snippet = build(:personal_snippet)
expect(commit.to_reference(another_snippet)).to eq "$#{personal_snippet.id}@#{commit.id}"
end
end
context 'with project snippet' do
let(:commit) { project_snippet.commit }
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq "$#{project_snippet.id}@#{commit.id}"
end
it 'supports a cross-snippet project reference' do
another_snippet = build(:personal_snippet)
expect(commit.to_reference(another_snippet)).to eq "#{project_snippet.project.path}$#{project_snippet.id}@#{commit.id}"
end
end
end
describe '.reference_valid?' do describe '.reference_valid?' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -264,6 +312,7 @@ describe Commit do ...@@ -264,6 +312,7 @@ describe Commit do
describe '#reference_link_text' do describe '#reference_link_text' do
let(:project) { create(:project, :repository, path: 'sample-project') } let(:project) { create(:project, :repository, path: 'sample-project') }
context 'with project' do
it 'returns a String reference to the object' do it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id expect(commit.reference_link_text).to eq commit.short_id
end end
...@@ -274,6 +323,33 @@ describe Commit do ...@@ -274,6 +323,33 @@ describe Commit do
end end
end end
context 'with personal snippet' do
let(:commit) { personal_snippet.commit }
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq "$#{personal_snippet.id}@#{commit.short_id}"
end
it 'supports a cross-snippet reference' do
another_snippet = build(:personal_snippet, :repository)
expect(commit.reference_link_text(another_snippet)).to eq "$#{personal_snippet.id}@#{commit.short_id}"
end
end
context 'with project snippet' do
let(:commit) { project_snippet.commit }
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq "$#{project_snippet.id}@#{commit.short_id}"
end
it 'supports a cross-snippet project reference' do
another_snippet = build(:project_snippet, :repository)
expect(commit.reference_link_text(another_snippet)).to eq "#{project_snippet.project.path}$#{project_snippet.id}@#{commit.short_id}"
end
end
end
describe '#title' do describe '#title' do
it "returns no_commit_message when safe_message is blank" do it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('') allow(commit).to receive(:safe_message).and_return('')
...@@ -401,6 +477,26 @@ eos ...@@ -401,6 +477,26 @@ eos
expect(commit.closes_issues).to be_empty expect(commit.closes_issues).to be_empty
end end
context 'with personal snippet' do
let(:commit) { personal_snippet.commit }
it 'does not call Gitlab::ClosingIssueExtractor' do
expect(Gitlab::ClosingIssueExtractor).not_to receive(:new)
commit.closes_issues
end
end
context 'with project snippet' do
let(:commit) { project_snippet.commit }
it 'does not call Gitlab::ClosingIssueExtractor' do
expect(Gitlab::ClosingIssueExtractor).not_to receive(:new)
commit.closes_issues
end
end
end end
it_behaves_like 'a mentionable' do it_behaves_like 'a mentionable' do
...@@ -597,19 +693,39 @@ eos ...@@ -597,19 +693,39 @@ eos
end end
describe '.from_hash' do describe '.from_hash' do
let(:new_commit) { described_class.from_hash(commit.to_hash, project) } subject { described_class.from_hash(commit.to_hash, container) }
shared_examples 'returns Commit' do
it 'returns a Commit' do it 'returns a Commit' do
expect(new_commit).to be_an_instance_of(described_class) expect(subject).to be_an_instance_of(described_class)
end end
it 'wraps a Gitlab::Git::Commit' do it 'wraps a Gitlab::Git::Commit' do
expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit) expect(subject.raw).to be_an_instance_of(Gitlab::Git::Commit)
end end
it 'stores the correct commit fields' do it 'stores the correct commit fields' do
expect(new_commit.id).to eq(commit.id) expect(subject.id).to eq(commit.id)
expect(new_commit.message).to eq(commit.message) expect(subject.message).to eq(commit.message)
end
end
context 'with project' do
let(:container) { project }
it_behaves_like 'returns Commit'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns Commit'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns Commit'
end end
end end
...@@ -670,6 +786,19 @@ eos ...@@ -670,6 +786,19 @@ eos
expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2) expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
expect(commit2.merge_requests).to contain_exactly(merge_request1) expect(commit2.merge_requests).to contain_exactly(merge_request1)
end end
context 'with personal snippet' do
it 'returns empty relation' do
expect(personal_snippet.repository.commit.merge_requests).to eq MergeRequest.none
end
end
context 'with project snippet' do
it 'returns empty relation' do
expect(project_snippet.project).not_to receive(:merge_requests)
expect(project_snippet.repository.commit.merge_requests).to eq MergeRequest.none
end
end
end end
describe 'signed commits' do describe 'signed commits' do
......
...@@ -16,4 +16,13 @@ describe PersonalSnippet do ...@@ -16,4 +16,13 @@ describe PersonalSnippet do
end end
end end
end end
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:personal_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" }
let(:expected_repository_klass) { Repository }
let(:expected_storage_klass) { Storage::Hashed }
let(:expected_web_url_path) { "snippets/#{container.id}" }
end
end end
...@@ -32,4 +32,13 @@ describe ProjectSnippet do ...@@ -32,4 +32,13 @@ describe ProjectSnippet do
end end
end end
end end
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" }
let(:expected_repository_klass) { Repository }
let(:expected_storage_klass) { Storage::Hashed }
let(:expected_web_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
end
end end
...@@ -113,6 +113,7 @@ describe Project do ...@@ -113,6 +113,7 @@ describe Project do
let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" } let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" }
let(:expected_repository_klass) { Repository } let(:expected_repository_klass) { Repository }
let(:expected_storage_klass) { Storage::Hashed } let(:expected_storage_klass) { Storage::Hashed }
let(:expected_web_url_path) { "#{container.namespace.full_path}/somewhere" }
end end
it 'has an inverse relationship with merge requests' do it 'has an inverse relationship with merge requests' do
......
# frozen_string_literal: true
require 'spec_helper'
describe SnippetRepository do
describe 'associations' do
it { is_expected.to belong_to(:shard) }
it { is_expected.to belong_to(:snippet) }
end
describe '.find_snippet' do
it 'finds snippet by disk path' do
snippet = create(:snippet)
snippet.track_snippet_repository
expect(described_class.find_snippet(snippet.disk_path)).to eq(snippet)
end
it 'returns nil when it does not find the snippet' do
expect(described_class.find_snippet('@@unexisting/path/to/snippet')).to be_nil
end
end
end
...@@ -19,6 +19,7 @@ describe Snippet do ...@@ -19,6 +19,7 @@ describe Snippet do
it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") } it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") }
it { is_expected.to have_one(:snippet_repository) }
end end
describe 'validation' do describe 'validation' do
...@@ -525,4 +526,109 @@ describe Snippet do ...@@ -525,4 +526,109 @@ describe Snippet do
snippet.to_json(params) snippet.to_json(params)
end end
end end
describe '#storage' do
let(:snippet) { create(:snippet) }
it "stores snippet in #{Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX} dir" do
expect(snippet.storage.disk_path).to start_with Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX
end
end
describe '#track_snippet_repository' do
let(:snippet) { create(:snippet, :repository) }
context 'when a snippet repository entry does not exist' do
it 'creates a new entry' do
expect { snippet.track_snippet_repository }.to change(snippet, :snippet_repository)
end
it 'tracks the snippet storage location' do
snippet.track_snippet_repository
expect(snippet.snippet_repository).to have_attributes(
disk_path: snippet.disk_path,
shard_name: snippet.repository_storage
)
end
end
context 'when a tracking entry exists' do
let!(:snippet_repository) { create(:snippet_repository, snippet: snippet) }
let!(:shard) { create(:shard, name: 'foo') }
it 'does not create a new entry in the database' do
expect { snippet.track_snippet_repository }.not_to change(snippet, :snippet_repository)
end
it 'updates the snippet storage location' do
allow(snippet).to receive(:disk_path).and_return('fancy/new/path')
allow(snippet).to receive(:repository_storage).and_return('foo')
snippet.track_snippet_repository
expect(snippet.snippet_repository).to have_attributes(
disk_path: 'fancy/new/path',
shard_name: 'foo'
)
end
end
end
describe '#create_repository' do
let(:snippet) { create(:snippet) }
it 'creates the repository' do
expect(snippet.repository).to receive(:after_create).and_call_original
expect(snippet.create_repository).to be_truthy
expect(snippet.repository.exists?).to be_truthy
end
it 'tracks snippet repository' do
expect do
snippet.create_repository
end.to change(SnippetRepository, :count).by(1)
end
context 'when repository exists' do
let(:snippet) { create(:snippet, :repository) }
it 'does not try to create repository' do
expect(snippet.repository).not_to receive(:after_create)
expect(snippet.create_repository).to be_nil
end
it 'does not track snippet repository' do
expect do
snippet.create_repository
end.not_to change(SnippetRepository, :count)
end
end
end
describe '#repository_storage' do
let(:snippet) { create(:snippet) }
it 'returns default repository storage' do
expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage)
snippet.repository_storage
end
context 'when snippet_project is already created' do
let!(:snippet_repository) { create(:snippet_repository, snippet: snippet) }
before do
allow(snippet_repository).to receive(:shard_name).and_return('foo')
end
it 'returns repository_storage from snippet_project' do
expect(Gitlab::CurrentSettings).not_to receive(:pick_repository_storage)
expect(snippet.repository_storage).to eq 'foo'
end
end
end
end end
...@@ -37,6 +37,8 @@ module FakeBlobHelpers ...@@ -37,6 +37,8 @@ module FakeBlobHelpers
end end
def fake_blob(**kwargs) def fake_blob(**kwargs)
Blob.decorate(FakeBlob.new(**kwargs), project) container = kwargs.delete(:container) || project
Blob.decorate(FakeBlob.new(**kwargs), container)
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
RSpec.shared_examples 'a repo type' do RSpec.shared_examples 'a repo type' do
describe '#identifier_for_container' do describe '#identifier_for_container' do
subject { described_class.identifier_for_container(project) } subject { described_class.identifier_for_container(expected_container) }
it { is_expected.to eq(expected_identifier) } it { is_expected.to eq(expected_identifier) }
end end
...@@ -35,7 +35,7 @@ RSpec.shared_examples 'a repo type' do ...@@ -35,7 +35,7 @@ RSpec.shared_examples 'a repo type' do
describe '#repository_for' do describe '#repository_for' do
it 'finds the repository for the repo type' do it 'finds the repository for the repo type' do
expect(described_class.repository_for(project)).to eq(expected_repository) expect(described_class.repository_for(expected_container)).to eq(expected_repository)
end end
end end
end end
...@@ -18,7 +18,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -18,7 +18,7 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { false } let(:only_path) { false }
it 'returns the full web URL for this repo' do it 'returns the full web URL for this repo' do
expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{expected_full_path}") expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}")
end end
end end
...@@ -26,7 +26,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -26,7 +26,7 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { true } let(:only_path) { true }
it 'returns the relative web URL for this repo' do it 'returns the relative web URL for this repo' do
expect(subject).to eq("/#{expected_full_path}") expect(subject).to eq("/#{expected_web_url_path}")
end end
end end
...@@ -34,14 +34,14 @@ RSpec.shared_examples 'model with repository' do ...@@ -34,14 +34,14 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { nil } let(:only_path) { nil }
it 'returns the full web URL for this repo' do it 'returns the full web URL for this repo' do
expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{expected_full_path}") expect(subject).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}")
end end
end end
end end
context 'when not given the only_path option' do context 'when not given the only_path option' do
it 'returns the full web URL for this repo' do it 'returns the full web URL for this repo' do
expect(container.web_url).to eq("#{Gitlab.config.gitlab.url}/#{expected_full_path}") expect(container.web_url).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}")
end end
end end
end end
...@@ -72,7 +72,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -72,7 +72,7 @@ RSpec.shared_examples 'model with repository' do
let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab/' } let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab/' }
it 'returns the url to the repo, with the root replaced with the custom one' do it 'returns the url to the repo, with the root replaced with the custom one' do
expect(subject).to eq("#{custom_http_clone_url_root}#{expected_full_path}.git") expect(subject).to eq("#{custom_http_clone_url_root}#{expected_web_url_path}.git")
end end
end end
...@@ -80,7 +80,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -80,7 +80,7 @@ RSpec.shared_examples 'model with repository' do
let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab' } let(:custom_http_clone_url_root) { 'https://git.example.com:51234/mygitlab' }
it 'returns the url to the repo, with the root replaced with the custom one' do it 'returns the url to the repo, with the root replaced with the custom one' do
expect(subject).to eq("#{custom_http_clone_url_root}/#{expected_full_path}.git") expect(subject).to eq("#{custom_http_clone_url_root}/#{expected_web_url_path}.git")
end end
end end
end end
...@@ -90,7 +90,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -90,7 +90,7 @@ RSpec.shared_examples 'model with repository' do
let(:custom_http_clone_url_root) { 'https://git.example.com:51234/' } let(:custom_http_clone_url_root) { 'https://git.example.com:51234/' }
it 'returns the url to the repo, with the root replaced with the custom one' do it 'returns the url to the repo, with the root replaced with the custom one' do
expect(subject).to eq("#{custom_http_clone_url_root}#{expected_full_path}.git") expect(subject).to eq("#{custom_http_clone_url_root}#{expected_web_url_path}.git")
end end
end end
...@@ -98,7 +98,7 @@ RSpec.shared_examples 'model with repository' do ...@@ -98,7 +98,7 @@ RSpec.shared_examples 'model with repository' do
let(:custom_http_clone_url_root) { 'https://git.example.com:51234' } let(:custom_http_clone_url_root) { 'https://git.example.com:51234' }
it 'returns the url to the repo, with the root replaced with the custom one' do it 'returns the url to the repo, with the root replaced with the custom one' do
expect(subject).to eq("#{custom_http_clone_url_root}/#{expected_full_path}.git") expect(subject).to eq("#{custom_http_clone_url_root}/#{expected_web_url_path}.git")
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment