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,7 +1143,11 @@ class Repository ...@@ -1143,7 +1143,11 @@ class Repository
end end
def project def project
container if repo_type.snippet?
container.project
else
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
......
...@@ -6,6 +6,8 @@ describe Blob do ...@@ -6,6 +6,8 @@ describe Blob do
include FakeBlobHelpers include FakeBlobHelpers
let(:project) { build(:project, lfs_enabled: true) } let(:project) { build(:project, lfs_enabled: true) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet, project: project) }
before do before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
...@@ -18,77 +20,146 @@ describe Blob do ...@@ -18,77 +20,146 @@ describe Blob do
end end
describe '.lazy' do 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(:commit_id) { 'e63f41fe459e62e1228fcef60d7189127aeba95a' }
let(:blob_size_limit) { 10 * 1024 * 1024 } let(:blob_size_limit) { 10 * 1024 * 1024 }
it 'does not fetch blobs when none are accessed' do shared_examples '.lazy checks' do
expect(project.repository).not_to receive(:blobs_at) it 'does not fetch blobs when none are accessed' do
expect(container.repository).not_to receive(:blobs_at)
described_class.lazy(project, commit_id, 'CHANGELOG') described_class.lazy(container, commit_id, 'CHANGELOG')
end end
it 'fetches all blobs for the same repository when one is accessed' do
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_container.repository).not_to receive(:blobs_at)
changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
described_class.lazy(other_container, commit_id, 'CHANGELOG')
# Access property so the values are loaded
changelog.id
contributing.id
end
it 'does not include blobs from previous requests in later requests' do
changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
it 'fetches all blobs for the same repository when one is accessed' do # Access property so the values are loaded
expect(project.repository).to receive(:blobs_at) changelog.id
.with([[commit_id, 'CHANGELOG'], [commit_id, 'CONTRIBUTING.md']], blob_size_limit: blob_size_limit) contributing.id
.once.and_call_original
expect(other_project.repository).not_to receive(:blobs_at)
changelog = described_class.lazy(project, commit_id, 'CHANGELOG') readme = described_class.lazy(container, commit_id, 'README.md')
contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
described_class.lazy(other_project, commit_id, 'CHANGELOG') expect(container.repository).to receive(:blobs_at)
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original
# Access property so the values are loaded readme.id
changelog.id end
contributing.id
end end
it 'does not include blobs from previous requests in later requests' do context 'with project' do
changelog = described_class.lazy(project, commit_id, 'CHANGELOG') let(:container) { create(:project, :repository) }
contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md') let(:same_container) { Project.find(container.id) }
let(:other_container) { create(:project, :repository) }
# Access property so the values are loaded it_behaves_like '.lazy checks'
changelog.id end
contributing.id
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) }
readme = described_class.lazy(project, commit_id, 'README.md') it_behaves_like '.lazy checks'
end
expect(project.repository).to receive(:blobs_at) context 'with project snippet' do
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original let(:container) { create(:project_snippet, :repository) }
let(:same_container) { ProjectSnippet.find(container.id) }
let(:other_container) { create(:project_snippet, :repository) }
readme.id it_behaves_like '.lazy checks'
end end
end end
describe '#data' do describe '#data' do
context 'using a binary blob' do shared_examples '#data checks' do
it 'returns the data as-is' do context 'using a binary blob' do
data = "\n\xFF\xB9\xC3" it 'returns the data as-is' do
blob = fake_blob(binary: true, data: data) data = "\n\xFF\xB9\xC3"
blob = fake_blob(binary: true, data: data, container: container)
expect(blob.data).to eq(data) expect(blob.data).to eq(data)
end
end end
end
context 'using a text blob' do context 'using a text blob' do
it 'converts the data to UTF-8' 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���") expect(blob.data).to eq("\n���")
end
end 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 end
describe '#external_storage_error?' do 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 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 context 'when the project has LFS enabled' do
it 'returns false' do context 'with project' do
expect(blob.external_storage_error?).to be_falsey 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
end end
...@@ -97,17 +168,39 @@ describe Blob do ...@@ -97,17 +168,39 @@ describe Blob do
project.lfs_enabled = false project.lfs_enabled = false
end end
it 'returns true' do context 'with project' do
expect(blob.external_storage_error?).to be_truthy 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 end
end end
context 'if the blob is not stored in LFS' do 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 context 'with project' do
expect(blob.external_storage_error?).to be_falsey 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 end
end end
...@@ -116,19 +209,59 @@ describe Blob do ...@@ -116,19 +209,59 @@ describe Blob do
context 'if the blob is stored in LFS' do 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) }
context 'when the project has LFS enabled' do shared_examples 'returns true' do
it 'returns true' do it do
expect(blob.stored_externally?).to be_truthy expect(blob.stored_externally?).to be_truthy
end end
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 context 'when the project does not have LFS enabled' do
before do before do
project.lfs_enabled = false project.lfs_enabled = false
end end
it 'returns false' do context 'with project' do
expect(blob.stored_externally?).to be_falsey 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 end
end end
...@@ -143,21 +276,63 @@ describe Blob do ...@@ -143,21 +276,63 @@ describe Blob do
end end
describe '#binary?' do 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 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 extension has a rich viewer' do
context 'if the viewer is binary' do context 'if the viewer is binary' do
it 'returns true' do let(:file) { 'file.pdf' }
blob = fake_blob(path: 'file.pdf', lfs: true)
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 end
context 'if the viewer is text-based' do context 'if the viewer is text-based' do
it 'return false' do let(:file) { 'file.md' }
blob = fake_blob(path: 'file.md', lfs: true)
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
expect(blob.binary?).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
end end
end end
...@@ -165,54 +340,138 @@ describe Blob do ...@@ -165,54 +340,138 @@ describe Blob do
context "if the extension doesn't have a rich viewer" 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 has a text mime type' do
context 'if the extension is for a programming language' do context 'if the extension is for a programming language' do
it 'returns false' do let(:file) { 'file.txt' }
blob = fake_blob(path: 'file.txt', lfs: true)
context 'with project' do
let(:container) { project }
it_behaves_like 'returns false'
end
expect(blob.binary?).to be_falsey 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 end
context 'if the extension is not for a programming language' do context 'if the extension is not for a programming language' do
it 'returns false' do let(:file) { 'file.ics' }
blob = fake_blob(path: 'file.ics', lfs: true)
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 }
expect(blob.binary?).to be_falsey it_behaves_like 'returns false'
end end
end end
end end
context 'if the extension has a binary mime type' do context 'if the extension has a binary mime type' do
context 'if the extension is for a programming language' do context 'if the extension is for a programming language' do
it 'returns false' do let(:file) { 'file.rb' }
blob = fake_blob(path: 'file.rb', lfs: true)
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 }
expect(blob.binary?).to be_falsey it_behaves_like 'returns false'
end end
end end
context 'if the extension is not for a programming language' do context 'if the extension is not for a programming language' do
it 'returns true' do let(:file) { 'file.exe' }
blob = fake_blob(path: 'file.exe', lfs: true)
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 end
end end
context 'if the extension has an unknown mime type' do context 'if the extension has an unknown mime type' do
context 'if the extension is for a programming language' do context 'if the extension is for a programming language' do
it 'returns false' do let(:file) { 'file.ini' }
blob = fake_blob(path: 'file.ini', lfs: true)
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 }
expect(blob.binary?).to be_falsey it_behaves_like 'returns false'
end end
end end
context 'if the extension is not for a programming language' do context 'if the extension is not for a programming language' do
it 'returns true' do let(:file) { 'file.wtf' }
blob = fake_blob(path: 'file.wtf', lfs: true)
context 'with project' do
let(:container) { project }
it_behaves_like 'returns true'
end
context 'with personal snippet' do
let(:container) { personal_snippet }
expect(blob.binary?).to be_truthy it_behaves_like 'returns true'
end
context 'with project snippet' do
let(:container) { project_snippet }
it_behaves_like 'returns true'
end end
end end
end end
...@@ -221,18 +480,46 @@ describe Blob do ...@@ -221,18 +480,46 @@ describe Blob do
context 'if the blob is not stored externally' do context 'if the blob is not stored externally' do
context 'if the blob is binary' do context 'if the blob is binary' do
it 'returns true' do let(:blob) { fake_blob(path: 'file.pdf', binary: true, container: container) }
blob = fake_blob(path: 'file.pdf', binary: true)
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 end
context 'if the blob is text-based' do context 'if the blob is text-based' do
it 'return false' do let(:blob) { fake_blob(path: 'file.md', container: container) }
blob = fake_blob(path: 'file.md')
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 }
expect(blob.binary?).to be_falsey it_behaves_like 'returns false'
end end
end end
end end
...@@ -389,38 +676,110 @@ describe Blob do ...@@ -389,38 +676,110 @@ describe Blob do
end end
describe '#rendered_as_text?' do 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 context 'when ignoring errors' do
let(:ignore_errors) { true }
context 'when the simple viewer is text-based' do context 'when the simple viewer is text-based' do
it 'returns true' do let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
blob = fake_blob(path: 'file.md', size: 100.megabytes)
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
end end
context 'when the simple viewer is binary' do context 'when the simple viewer is binary' do
it 'returns false' do let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes, container: container) }
blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
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 }
expect(blob.rendered_as_text?).to be_falsey it_behaves_like 'returns false'
end end
end end
end end
context 'when not ignoring errors' do context 'when not ignoring errors' do
let(:ignore_errors) { false }
context 'when the viewer has render errors' do context 'when the viewer has render errors' do
it 'returns false' do let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
blob = fake_blob(path: 'file.md', size: 100.megabytes)
expect(blob.rendered_as_text?(ignore_errors: false)).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 end
context "when the viewer doesn't have render errors" do context "when the viewer doesn't have render errors" do
it 'returns true' do let(:blob) { fake_blob(path: 'file.md', container: container) }
blob = fake_blob(path: 'file.md')
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 end
end end
......
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
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(:commit) { project.commit } 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 describe 'modules' do
subject { described_class } subject { described_class }
...@@ -17,49 +19,67 @@ describe Commit do ...@@ -17,49 +19,67 @@ 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
let(:oids) do
%w(
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
)
end
context 'when the commits are found' do subject { oids.map { |oid| described_class.lazy(container, oid) } }
let(:oids) do
%w(
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
)
end
subject { oids.map { |oid| described_class.lazy(project, oid) } } it 'batches requests for commits' do
expect(container.repository).to receive(:commits_by).once.and_call_original
it 'batches requests for commits' do subject.first.title
expect(project.repository).to receive(:commits_by).once.and_call_original subject.last.title
end
subject.first.title it 'maintains ordering' do
subject.last.title subject.each_with_index do |commit, i|
end expect(commit.id).to eq(oids[i])
end
end
it 'maintains ordering' do it 'does not attempt to replace methods via BatchLoader' do
subject.each_with_index do |commit, i| subject.each do |commit|
expect(commit.id).to eq(oids[i]) expect(commit).to receive(:method_missing).and_call_original
commit.id
end
end end
end end
it 'does not attempt to replace methods via BatchLoader' do context 'when not found' do
subject.each do |commit| it 'returns nil as commit' do
expect(commit).to receive(:method_missing).and_call_original commit = described_class.lazy(container, 'deadbeef').__sync
commit.id expect(commit).to be_nil
end end
end end
end end
context 'when not found' do context 'with project' do
it 'returns nil as commit' do let(:container) { project }
commit = described_class.lazy(project, 'deadbeef').__sync
expect(commit).to be_nil it_behaves_like '.lazy checks'
end 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
end end
...@@ -231,15 +251,43 @@ describe Commit do ...@@ -231,15 +251,43 @@ describe Commit do
end end
describe '#to_reference' do describe '#to_reference' do
let(:project) { create(:project, :repository, path: 'sample-project') } context 'with project' do
let(:project) { create(:project, :repository, path: 'sample-project') }
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
end
it 'returns a String reference to the object' do it 'supports a cross-project reference' do
expect(commit.to_reference).to eq commit.id another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
end
end end
it 'supports a cross-project reference' do context 'with personal snippet' do
another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace) let(:commit) { personal_snippet.commit }
expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
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
end end
...@@ -264,13 +312,41 @@ describe Commit do ...@@ -264,13 +312,41 @@ 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') }
it 'returns a String reference to the object' do context 'with project' do
expect(commit.reference_link_text).to eq commit.short_id it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
end
it 'supports a cross-project reference' do
another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}"
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 end
it 'supports a cross-project reference' do context 'with project snippet' do
another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace) let(:commit) { project_snippet.commit }
expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}"
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
end end
...@@ -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) }
it 'returns a Commit' do shared_examples 'returns Commit' do
expect(new_commit).to be_an_instance_of(described_class) it 'returns a Commit' do
expect(subject).to be_an_instance_of(described_class)
end
it 'wraps a Gitlab::Git::Commit' do
expect(subject.raw).to be_an_instance_of(Gitlab::Git::Commit)
end
it 'stores the correct commit fields' do
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 end
it 'wraps a Gitlab::Git::Commit' do context 'with personal snippet' do
expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit) let(:container) { personal_snippet }
it_behaves_like 'returns Commit'
end end
it 'stores the correct commit fields' do context 'with project snippet' do
expect(new_commit.id).to eq(commit.id) let(:container) { project_snippet }
expect(new_commit.message).to eq(commit.message)
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