Commit 75a16ad3 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'fj-create-snippet-repository-model' into 'master'

Create snippet repository model

See merge request gitlab-org/gitlab!23796
parents 1fc8efd1 e9d6dcc1
......@@ -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
......@@ -1131,7 +1131,11 @@ class Repository
end
def project
container
if repo_type.snippet?
container.project
else
container
end
end
private
......@@ -1145,7 +1149,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_12_052620) 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_12_052620) 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,77 +20,146 @@ 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 }
it 'does not fetch blobs when none are accessed' do
expect(project.repository).not_to receive(:blobs_at)
shared_examples '.lazy checks' do
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')
end
described_class.lazy(container, commit_id, 'CHANGELOG')
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
expect(project.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)
# Access property so the values are loaded
changelog.id
contributing.id
changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
readme = described_class.lazy(container, commit_id, 'README.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
changelog.id
contributing.id
readme.id
end
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')
context 'with project' do
let(:container) { create(:project, :repository) }
let(:same_container) { Project.find(container.id) }
let(:other_container) { create(:project, :repository) }
# Access property so the values are loaded
changelog.id
contributing.id
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) }
readme = described_class.lazy(project, commit_id, 'README.md')
it_behaves_like '.lazy checks'
end
expect(project.repository).to receive(:blobs_at)
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original
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) }
readme.id
it_behaves_like '.lazy checks'
end
end
describe '#data' 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)
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, container: container)
expect(blob.data).to eq(data)
expect(blob.data).to eq(data)
end
end
end
context 'using a text blob' do
it 'converts the data to UTF-8' do
blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
context 'using a text blob' do
it 'converts the data to UTF-8' do
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
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' }
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
......@@ -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' }
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
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' }
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
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' }
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
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' }
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
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' }
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
......@@ -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) }
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
......@@ -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) }
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
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) }
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
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,8 +3,10 @@
require 'spec_helper'
describe Commit do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.commit }
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
subject { described_class }
......@@ -17,49 +19,67 @@ 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(
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
)
end
context 'when the commits are found' do
let(:oids) do
%w(
498214de67004b1da3d820901307bed2a68a8ef6
c642fe9b8b9f28f9225d7ea953fe14e74748d53b
6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
048721d90c449b244b7b4c53a9186b04330174ec
281d3a76f31c812dbf48abce82ccf6860adedd81
)
end
subject { oids.map { |oid| described_class.lazy(container, oid) } }
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
expect(project.repository).to receive(:commits_by).once.and_call_original
subject.first.title
subject.last.title
end
subject.first.title
subject.last.title
end
it 'maintains ordering' do
subject.each_with_index do |commit, i|
expect(commit.id).to eq(oids[i])
end
end
it 'maintains ordering' do
subject.each_with_index do |commit, i|
expect(commit.id).to eq(oids[i])
it 'does not attempt to replace methods via BatchLoader' do
subject.each do |commit|
expect(commit).to receive(:method_missing).and_call_original
commit.id
end
end
end
it 'does not attempt to replace methods via BatchLoader' do
subject.each do |commit|
expect(commit).to receive(:method_missing).and_call_original
context 'when not found' do
it 'returns nil as commit' do
commit = described_class.lazy(container, 'deadbeef').__sync
commit.id
expect(commit).to be_nil
end
end
end
context 'when not found' do
it 'returns nil as commit' do
commit = described_class.lazy(project, 'deadbeef').__sync
context 'with project' do
let(:container) { project }
expect(commit).to be_nil
end
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
......@@ -231,15 +251,43 @@ describe Commit do
end
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
expect(commit.to_reference).to eq commit.id
it 'supports a cross-project reference' do
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
it 'supports a cross-project reference' do
another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
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
......@@ -264,13 +312,41 @@ describe Commit do
describe '#reference_link_text' do
let(:project) { create(:project, :repository, path: 'sample-project') }
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
context 'with project' do
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
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}"
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
......@@ -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) }
it 'returns a Commit' do
expect(new_commit).to be_an_instance_of(described_class)
shared_examples 'returns Commit' do
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
it 'wraps a Gitlab::Git::Commit' do
expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit)
context 'with personal snippet' do
let(:container) { personal_snippet }
it_behaves_like 'returns 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)
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