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
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
def closes_issues(current_user = self.committer)
return unless repository.repo_type.project?
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
......@@ -297,7 +299,11 @@ class Commit
end
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
def method_missing(method, *args, &block)
......@@ -507,7 +513,7 @@ class Commit
end
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?
"#{base}#{self.class.reference_prefix}#{referable_commit_id}"
......
# 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
extend ActiveSupport::Concern
include Gitlab::ShellAdapter
include AfterCommitQueue
include Referable
include Gitlab::Utils::StrongMemoize
delegate :base_dir, :disk_path, to: :storage
......
......@@ -2,4 +2,8 @@
class PersonalSnippet < Snippet
include WithUploads
def web_url(only_path: nil)
Gitlab::Routing.url_helpers.snippet_url(self, only_path: only_path)
end
end
......@@ -9,7 +9,6 @@ class Project < ApplicationRecord
include AccessRequestable
include Avatarable
include CacheMarkdownField
include Referable
include Sortable
include AfterCommitQueue
include CaseSensitivity
......
......@@ -5,4 +5,8 @@ class ProjectSnippet < Snippet
validates :project, presence: true
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
......@@ -1143,8 +1143,12 @@ class Repository
end
def project
if repo_type.snippet?
container.project
else
container
end
end
private
......@@ -1157,7 +1161,7 @@ class Repository
Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end
::Commit.new(commit, project) if commit
::Commit.new(commit, container) if commit
end
def cache
......
......@@ -6,7 +6,6 @@ class Snippet < ApplicationRecord
include CacheMarkdownField
include Noteable
include Participable
include Referable
include Sortable
include Awardable
include Mentionable
......@@ -15,10 +14,11 @@ class Snippet < ApplicationRecord
include Gitlab::SQL::Pattern
include FromUnion
include IgnorableColumns
include HasRepository
extend ::Gitlab::Utils::Override
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 :description
......@@ -42,6 +42,7 @@ class Snippet < ApplicationRecord
has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "SnippetUserMention"
has_one :snippet_repository, inverse_of: :snippet
delegate :name, :email, to: :author, prefix: true, allow_nil: true
......@@ -254,6 +255,47 @@ class Snippet < ApplicationRecord
super
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
# 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 @@
module Storage
class Hashed
attr_accessor :project
delegate :gitlab_shell, :repository_storage, to: :project
attr_accessor :container
delegate :gitlab_shell, :repository_storage, to: :container
REPOSITORY_PATH_PREFIX = '@hashed'
SNIPPET_REPOSITORY_PATH_PREFIX = '@snippets'
POOL_PATH_PREFIX = '@pools'
def initialize(project, prefix: REPOSITORY_PATH_PREFIX)
@project = project
def initialize(container, prefix: REPOSITORY_PATH_PREFIX)
@container = container
@prefix = prefix
end
......@@ -20,9 +21,10 @@ module Storage
"#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
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
"#{base_dir}/#{disk_hash}" if disk_hash
end
......@@ -33,10 +35,10 @@ module Storage
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`
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
---
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
t.index ["user_id"], name: "index_smartcard_identities_on_user_id"
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|
t.integer "snippet_id", null: false
t.integer "note_id"
......@@ -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 "slack_integrations", "services", 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", "snippets", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
......
......@@ -17,6 +17,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class).to be_design
expect(described_class).not_to be_project
expect(described_class).not_to be_wiki
expect(described_class).not_to be_snippet
end
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
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) }
).freeze
TYPES = {
PROJECT.name.to_s => PROJECT,
WIKI.name.to_s => WIKI
WIKI.name.to_s => WIKI,
SNIPPET.name.to_s => SNIPPET
}.freeze
def self.types
......
......@@ -47,6 +47,10 @@ module Gitlab
self == PROJECT
end
def snippet?
self == SNIPPET
end
def path_suffix
suffix ? ".#{suffix}" : ''
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
trait :private do
visibility_level { Snippet::PRIVATE }
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
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'
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::PROJECT do
it_behaves_like 'a repo type' do
......@@ -16,6 +18,7 @@ describe Gitlab::GlRepository::RepoType do
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
end
it 'checks if repository path is valid' do
......@@ -36,6 +39,7 @@ describe Gitlab::GlRepository::RepoType do
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
end
it 'checks if repository path is valid' do
......@@ -43,4 +47,38 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(project.wiki.repository.full_path)).to be_truthy
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
......@@ -91,6 +91,7 @@ snippets:
- award_emoji
- user_agent_detail
- user_mentions
- snippet_repository
releases:
- author
- project
......
......@@ -6,6 +6,8 @@ describe Blob do
include FakeBlobHelpers
let(:project) { build(:project, lfs_enabled: true) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet, project: project) }
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
......@@ -18,28 +20,26 @@ describe Blob do
end
describe '.lazy' do
let(:project) { create(:project, :repository) }
let(:same_project) { Project.find(project.id) }
let(:other_project) { create(:project, :repository) }
let(:commit_id) { 'e63f41fe459e62e1228fcef60d7189127aeba95a' }
let(:blob_size_limit) { 10 * 1024 * 1024 }
shared_examples '.lazy checks' do
it 'does not fetch blobs when none are accessed' do
expect(project.repository).not_to receive(:blobs_at)
expect(container.repository).not_to receive(:blobs_at)
described_class.lazy(project, commit_id, 'CHANGELOG')
described_class.lazy(container, commit_id, 'CHANGELOG')
end
it 'fetches all blobs for the same repository when one is accessed' do
expect(project.repository).to receive(:blobs_at)
expect(container.repository).to receive(:blobs_at)
.with([[commit_id, 'CHANGELOG'], [commit_id, 'CONTRIBUTING.md']], blob_size_limit: blob_size_limit)
.once.and_call_original
expect(other_project.repository).not_to receive(:blobs_at)
expect(other_container.repository).not_to receive(:blobs_at)
changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
described_class.lazy(other_project, commit_id, 'CHANGELOG')
described_class.lazy(other_container, commit_id, 'CHANGELOG')
# Access property so the values are loaded
changelog.id
......@@ -47,27 +47,53 @@ describe Blob do
end
it 'does not include blobs from previous requests in later requests' do
changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
# Access property so the values are loaded
changelog.id
contributing.id
readme = described_class.lazy(project, commit_id, 'README.md')
readme = described_class.lazy(container, commit_id, 'README.md')
expect(project.repository).to receive(:blobs_at)
expect(container.repository).to receive(:blobs_at)
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original
readme.id
end
end
context 'with project' do
let(:container) { create(:project, :repository) }
let(:same_container) { Project.find(container.id) }
let(:other_container) { create(:project, :repository) }
it_behaves_like '.lazy checks'
end
context 'with personal snippet' do
let(:container) { create(:personal_snippet, :repository) }
let(:same_container) { PersonalSnippet.find(container.id) }
let(:other_container) { create(:personal_snippet, :repository) }
it_behaves_like '.lazy checks'
end
context 'with project snippet' do
let(:container) { create(:project_snippet, :repository) }
let(:same_container) { ProjectSnippet.find(container.id) }
let(:other_container) { create(:project_snippet, :repository) }
it_behaves_like '.lazy checks'
end
end
describe '#data' do
shared_examples '#data checks' do
context 'using a binary blob' do
it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3"
blob = fake_blob(binary: true, data: data)
blob = fake_blob(binary: true, data: data, container: container)
expect(blob.data).to eq(data)
end
......@@ -75,20 +101,65 @@ describe Blob do
context 'using a text blob' do
it 'converts the data to UTF-8' do
blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3", container: container)
expect(blob.data).to eq("\n���")
end
end
end
context 'with project' do
let(:container) { project }
it_behaves_like '#data checks'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like '#data checks'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like '#data checks'
end
end
describe '#external_storage_error?' do
shared_examples 'no error' do
it do
expect(blob.external_storage_error?).to be_falsey
end
end
shared_examples 'returns error' do
it do
expect(blob.external_storage_error?).to be_truthy
end
end
context 'if the blob is stored in LFS' do
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
let(:blob) { fake_blob(path: 'file.pdf', lfs: true, container: container) }
context 'when the project has LFS enabled' do
it 'returns false' do
expect(blob.external_storage_error?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'no error'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns error'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'no error'
end
end
......@@ -97,17 +168,39 @@ describe Blob do
project.lfs_enabled = false
end
it 'returns true' do
expect(blob.external_storage_error?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns error'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns error'
end
end
end
context 'if the blob is not stored in LFS' do
let(:blob) { fake_blob(path: 'file.md') }
let(:blob) { fake_blob(path: 'file.md', container: container) }
it 'returns false' do
expect(blob.external_storage_error?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'no error'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'no error'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'no error'
end
end
end
......@@ -116,19 +209,59 @@ describe Blob do
context 'if the blob is stored in LFS' do
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
context 'when the project has LFS enabled' do
it 'returns true' do
shared_examples 'returns true' do
it do
expect(blob.stored_externally?).to be_truthy
end
end
shared_examples 'returns false' do
it do
expect(blob.stored_externally?).to be_falsey
end
end
context 'when the project has LFS enabled' do
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
context 'when the project does not have LFS enabled' do
before do
project.lfs_enabled = false
end
it 'returns false' do
expect(blob.stored_externally?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
end
......@@ -143,21 +276,63 @@ describe Blob do
end
describe '#binary?' do
shared_examples 'returns true' do
it do
expect(blob.binary?).to be_truthy
end
end
shared_examples 'returns false' do
it do
expect(blob.binary?).to be_falsey
end
end
context 'if the blob is stored externally' do
let(:blob) { fake_blob(path: file, lfs: true) }
context 'if the extension has a rich viewer' do
context 'if the viewer is binary' do
it 'returns true' do
blob = fake_blob(path: 'file.pdf', lfs: true)
let(:file) { 'file.pdf' }
expect(blob.binary?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
context 'if the viewer is text-based' do
it 'return false' do
blob = fake_blob(path: 'file.md', lfs: true)
let(:file) { 'file.md' }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
end
......@@ -165,54 +340,138 @@ describe Blob do
context "if the extension doesn't have a rich viewer" do
context 'if the extension has a text mime type' do
context 'if the extension is for a programming language' do
it 'returns false' do
blob = fake_blob(path: 'file.txt', lfs: true)
let(:file) { 'file.txt' }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
context 'if the extension is not for a programming language' do
it 'returns false' do
blob = fake_blob(path: 'file.ics', lfs: true)
let(:file) { 'file.ics' }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
end
context 'if the extension has a binary mime type' do
context 'if the extension is for a programming language' do
it 'returns false' do
blob = fake_blob(path: 'file.rb', lfs: true)
let(:file) { 'file.rb' }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
context 'if the extension is not for a programming language' do
it 'returns true' do
blob = fake_blob(path: 'file.exe', lfs: true)
let(:file) { 'file.exe' }
expect(blob.binary?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
end
context 'if the extension has an unknown mime type' do
context 'if the extension is for a programming language' do
it 'returns false' do
blob = fake_blob(path: 'file.ini', lfs: true)
let(:file) { 'file.ini' }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
context 'if the extension is not for a programming language' do
it 'returns true' do
blob = fake_blob(path: 'file.wtf', lfs: true)
let(:file) { 'file.wtf' }
expect(blob.binary?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
end
......@@ -221,18 +480,46 @@ describe Blob do
context 'if the blob is not stored externally' do
context 'if the blob is binary' do
it 'returns true' do
blob = fake_blob(path: 'file.pdf', binary: true)
let(:blob) { fake_blob(path: 'file.pdf', binary: true, container: container) }
expect(blob.binary?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
context 'if the blob is text-based' do
it 'return false' do
blob = fake_blob(path: 'file.md')
let(:blob) { fake_blob(path: 'file.md', container: container) }
expect(blob.binary?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
end
......@@ -389,38 +676,110 @@ describe Blob do
end
describe '#rendered_as_text?' do
shared_examples 'returns true' do
it do
expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_truthy
end
end
shared_examples 'returns false' do
it do
expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_falsey
end
end
context 'when ignoring errors' do
let(:ignore_errors) { true }
context 'when the simple viewer is text-based' do
it 'returns true' do
blob = fake_blob(path: 'file.md', size: 100.megabytes)
let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
expect(blob.rendered_as_text?).to be_truthy
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end
end
context 'when the simple viewer is binary' do
it 'returns false' do
blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes, container: container) }
expect(blob.rendered_as_text?).to be_falsey
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
end
context 'when not ignoring errors' do
let(:ignore_errors) { false }
context 'when the viewer has render errors' do
it 'returns false' do
blob = fake_blob(path: 'file.md', size: 100.megabytes)
let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
it_behaves_like 'returns false'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns false'
end
end
context "when the viewer doesn't have render errors" do
it 'returns true' do
blob = fake_blob(path: 'file.md')
let(:blob) { fake_blob(path: 'file.md', container: container) }
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
it_behaves_like 'returns true'
end
end
end
......
......@@ -3,7 +3,9 @@
require 'spec_helper'
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 }
describe 'modules' do
......@@ -17,8 +19,7 @@ describe Commit do
end
describe '.lazy' do
let_it_be(:project) { create(:project, :repository) }
shared_examples '.lazy checks' do
context 'when the commits are found' do
let(:oids) do
%w(
......@@ -30,10 +31,10 @@ describe Commit do
)
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
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.last.title
......@@ -56,13 +57,32 @@ describe Commit do
context 'when not found' 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
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
it 'is equal to itself' do
expect(commit.diff_refs).to eq(commit.diff_refs)
......@@ -231,6 +251,7 @@ describe Commit do
end
describe '#to_reference' do
context 'with project' do
let(:project) { create(:project, :repository, path: 'sample-project') }
it 'returns a String reference to the object' do
......@@ -243,6 +264,33 @@ describe Commit do
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
using RSpec::Parameterized::TableSyntax
......@@ -264,6 +312,7 @@ describe Commit do
describe '#reference_link_text' do
let(:project) { create(:project, :repository, path: 'sample-project') }
context 'with project' do
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
end
......@@ -274,6 +323,33 @@ describe Commit do
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
it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('')
......@@ -401,6 +477,26 @@ eos
expect(commit.closes_issues).to be_empty
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
it_behaves_like 'a mentionable' do
......@@ -597,19 +693,39 @@ eos
end
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
expect(new_commit).to be_an_instance_of(described_class)
expect(subject).to be_an_instance_of(described_class)
end
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
it 'stores the correct commit fields' do
expect(new_commit.id).to eq(commit.id)
expect(new_commit.message).to eq(commit.message)
expect(subject.id).to eq(commit.id)
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
......@@ -670,6 +786,19 @@ eos
expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
expect(commit2.merge_requests).to contain_exactly(merge_request1)
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
describe 'signed commits' do
......
......@@ -16,4 +16,13 @@ describe PersonalSnippet do
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
......@@ -32,4 +32,13 @@ describe ProjectSnippet do
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
......@@ -113,6 +113,7 @@ describe Project do
let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" }
let(:expected_repository_klass) { Repository }
let(:expected_storage_klass) { Storage::Hashed }
let(:expected_web_url_path) { "#{container.namespace.full_path}/somewhere" }
end
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
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(:user_mentions).class_name("SnippetUserMention") }
it { is_expected.to have_one(:snippet_repository) }
end
describe 'validation' do
......@@ -525,4 +526,109 @@ describe Snippet do
snippet.to_json(params)
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
......@@ -37,6 +37,8 @@ module FakeBlobHelpers
end
def fake_blob(**kwargs)
Blob.decorate(FakeBlob.new(**kwargs), project)
container = kwargs.delete(:container) || project
Blob.decorate(FakeBlob.new(**kwargs), container)
end
end
......@@ -2,7 +2,7 @@
RSpec.shared_examples 'a repo type' 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) }
end
......@@ -35,7 +35,7 @@ RSpec.shared_examples 'a repo type' do
describe '#repository_for' 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
......@@ -18,7 +18,7 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { false }
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
......@@ -26,7 +26,7 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { true }
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
......@@ -34,14 +34,14 @@ RSpec.shared_examples 'model with repository' do
let(:only_path) { nil }
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
context 'when not given the only_path option' 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
......@@ -72,7 +72,7 @@ RSpec.shared_examples 'model with repository' do
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
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
......@@ -80,7 +80,7 @@ RSpec.shared_examples 'model with repository' do
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
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
......@@ -90,7 +90,7 @@ RSpec.shared_examples 'model with repository' do
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
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
......@@ -98,7 +98,7 @@ RSpec.shared_examples 'model with repository' do
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
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
......
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