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
......
This diff is collapsed.
This diff is collapsed.
......@@ -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