Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-13 18:16:51 +00:00
parent bd5ff5cb65
commit aa072fd68c
28 changed files with 316 additions and 181 deletions

View file

@ -175,8 +175,8 @@ That's all of the required database changes.
#### Step 1. Implement replication and verification
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Gitlab::Geo::VerificationState` concern.
- Include `::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Geo::VerifiableModel` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
- Implement the `verification_state_object` method to return the object that holds
@ -192,8 +192,8 @@ That's all of the required database changes.
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::ReplicableModel
include ::Gitlab::Geo::VerificationState
include ::Geo::ReplicableModel
include ::Geo::VerifiableModel
with_replicator Geo::CoolWidgetReplicator

View file

@ -179,8 +179,8 @@ That's all of the required database changes.
#### Step 1. Implement replication and verification
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Gitlab::Geo::VerificationState` concern.
- Include `::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Geo::VerifiableModel` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
- Implement the `verification_state_object` method to return the object that holds
@ -194,8 +194,8 @@ That's all of the required database changes.
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::ReplicableModel
include ::Gitlab::Geo::VerificationState
include ::Geo::ReplicableModel
include ::Geo::VerifiableModel
with_replicator Geo::CoolWidgetReplicator

View file

@ -98,10 +98,7 @@ gem 'rack-cors', '~> 1.0.6', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.11.10'
# NOTE: graphiql-rails v1.5+ doesn't work: https://gitlab.com/gitlab-org/gitlab/issues/31771
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
# https://gitlab.com/gitlab-org/gitlab/issues/31747
gem 'graphiql-rails', '~> 1.4.10'
gem 'graphiql-rails', '~> 1.8'
gem 'apollo_upload_server', '~> 2.1.0'
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
gem 'graphlient', '~> 0.4.0' # Used by BulkImport feature (group::import)

View file

@ -561,7 +561,7 @@ GEM
grape_logging (1.8.3)
grape
rack
graphiql-rails (1.4.10)
graphiql-rails (1.8.0)
railties
sprockets-rails
graphlient (0.4.0)
@ -1498,7 +1498,7 @@ DEPENDENCIES
grape-entity (~> 0.10.0)
grape-path-helpers (~> 1.7.0)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphiql-rails (~> 1.8)
graphlient (~> 0.4.0)
graphql (~> 1.11.10)
graphql-docs (~> 1.6.0)

View file

@ -60,10 +60,7 @@ export const trackSaasTrialSubmit = () => {
return;
}
const form = document.getElementById('new_trial');
form.addEventListener('submit', () => {
pushEvent('saasTrialSubmit');
});
pushEvent('saasTrialSubmit');
};
export const trackSaasTrialSkip = () => {

View file

@ -3,6 +3,7 @@ import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@ -172,18 +173,27 @@ export default {
},
methods: {
tabCount({ runnerType }) {
let count;
switch (runnerType) {
case null:
return this.allRunnersCount;
count = this.allRunnersCount;
break;
case INSTANCE_TYPE:
return this.instanceRunnersCount;
count = this.instanceRunnersCount;
break;
case GROUP_TYPE:
return this.groupRunnersCount;
count = this.groupRunnersCount;
break;
case PROJECT_TYPE:
return this.projectRunnersCount;
count = this.projectRunnersCount;
break;
default:
return null;
}
if (typeof count === 'number') {
return formatNumber(count);
}
return '';
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@ -208,7 +218,7 @@ export default {
>
<template #title="{ tab }">
{{ tab.title }}
<gl-badge v-if="typeof tabCount(tab) == 'number'" class="gl-ml-1" size="sm">
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
{{ tabCount(tab) }}
</gl-badge>
</template>

View file

@ -2,7 +2,10 @@
import { escape } from 'lodash';
import { __ } from '~/locale';
import { WI_TITLE_TRACK_LABEL } from '../constants';
export default {
WI_TITLE_TRACK_LABEL,
props: {
initialTitle: {
type: String,
@ -56,6 +59,7 @@ export default {
role="textbox"
:aria-label="__('Title')"
:data-placeholder="placeholder"
:data-track-label="$options.WI_TITLE_TRACK_LABEL"
:contenteditable="!disabled"
class="gl-pseudo-placeholder"
@blur="handleBlur"

View file

@ -1,3 +1,5 @@
export const widgetTypes = {
title: 'TITLE',
};
export const WI_TITLE_TRACK_LABEL = 'item_title';

View file

@ -1,16 +1,21 @@
<script>
import { GlAlert } from '@gitlab/ui';
import Tracking from '~/tracking';
import workItemQuery from '../graphql/work_item.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { widgetTypes } from '../constants';
import { widgetTypes, WI_TITLE_TRACK_LABEL } from '../constants';
import ItemTitle from '../components/item_title.vue';
const trackingMixin = Tracking.mixin();
export default {
titleUpdatedEvent: 'updated_title',
components: {
ItemTitle,
GlAlert,
},
mixins: [trackingMixin],
props: {
id: {
type: String,
@ -34,6 +39,14 @@ export default {
},
},
computed: {
tracking() {
return {
category: 'workItems:show',
action: 'updated_title',
label: WI_TITLE_TRACK_LABEL,
property: '[type_work_item]',
};
},
titleWidgetData() {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
},
@ -50,6 +63,7 @@ export default {
},
},
});
this.track();
} catch {
this.error = true;
}

View file

@ -10,6 +10,8 @@
.flash-container {
margin-bottom: $gl-padding;
position: relative;
top: 8px;
}
.brand-holder {

View file

@ -548,6 +548,8 @@ svg {
}
.login-page .flash-container {
margin-bottom: 16px;
position: relative;
top: 8px;
}
.login-page .brand-holder {
font-size: 18px;

View file

@ -81,8 +81,7 @@ module Projects
@protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new
@protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size }
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size }
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tag_names).size }
load_gon_index
end

View file

@ -2,6 +2,10 @@
class ProtectableDropdown
REF_TYPES = %i[branches tags].freeze
REF_NAME_METHODS = {
branches: :branch_names,
tags: :tag_names
}.freeze
def initialize(project, ref_type)
raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES)
@ -23,12 +27,12 @@ class ProtectableDropdown
private
def refs
@project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend
def ref_names
@project.repository.public_send(ref_name_method) # rubocop:disable GitlabSecurity/PublicSend
end
def ref_names
refs.map(&:name)
def ref_name_method
REF_NAME_METHODS[@ref_type]
end
def protections

View file

@ -5,10 +5,10 @@ class RefMatcher
@ref_name_or_pattern = ref_name_or_pattern
end
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`] or their names [`String`])
# that match the current protected ref.
def matching(refs)
refs.select { |ref| matches?(ref.name) }
refs.select { |ref| ref.is_a?(String) ? matches?(ref) : matches?(ref.name) }
end
# Checks if the protected ref matches the given ref name.

View file

@ -1,99 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>GraphiQL</title>
<%= stylesheet_link_tag("graphiql/rails/application") %>
<%# TODO: This file was included to fix a CSP failure. Please remove when https://github.com/rmosolgo/graphiql-rails/pull/71 will be released %>
<%= javascript_include_tag("graphiql/rails/application", nonce: true) %>
</head>
<body>
<div id="graphiql-container">
Loading...
</div>
<%= javascript_tag nonce: true do -%>
var parameters = {};
<% if GraphiQL::Rails.config.query_params %>
// Parse the search string to get url parameters.
var search = window.location.search;
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
<% end %>
// Defines a GraphQL fetcher using the fetch API.
var graphQLEndpoint = "<%= graphql_endpoint_path %>";
function graphQLFetcher(graphQLParams) {
return fetch(graphQLEndpoint, {
method: 'post',
headers: <%= raw JSON.pretty_generate(GraphiQL::Rails.config.resolve_headers(self)) %>,
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function(response) {
return response.text();
}).then(function(text) {
try {
return JSON.parse(text);
} catch(error) {
return {
"message": "The server responded with invalid JSON, this is probably a server-side error",
"response": text,
};
}
})
}
<% if GraphiQL::Rails.config.initial_query %>
var defaultQuery = "<%= GraphiQL::Rails.config.initial_query.gsub("\n", '\n').gsub('"', '\"').html_safe %>";
<% else %>
var defaultQuery = undefined
<% end %>
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
defaultQuery: defaultQuery,
<% if GraphiQL::Rails.config.query_params %>
query: parameters.query,
variables: parameters.variables,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables
<% end %>
}),
document.getElementById("graphiql-container")
);
<% end -%>
</body>
</html>

View file

@ -1,7 +1,7 @@
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.card-header.bg-white
= s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }
= s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: 0 }
%p.settings-message.text-center
= s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.")
- else

View file

@ -9,7 +9,7 @@
%div
- if protected_branch.wildcard?
- matching_branches = protected_branch.matching(repository.branches)
- matching_branches = protected_branch.matching(repository.branch_names)
= link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
- elsif !protected_branch.commit
%span.text-muted Branch was deleted.

View file

@ -6,7 +6,7 @@
= gl_badge_tag s_('ProtectedTags|default'), variant: :info, class: 'gl-ml-2'
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
- matching_tags = protected_tag.matching(repository.tag_names)
= link_to pluralize(matching_tags.count, "matching tag"), project_protected_tag_path(@project, protected_tag)
- else
- if commit = protected_tag.commit

View file

@ -1,7 +1,7 @@
.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.card-header
Protected tags (#{@protected_tags_count})
Protected tags (0)
%p.settings-message.text-center
No tags are protected.
- else

View file

@ -0,0 +1,9 @@
---
redirect_to: 'gitlab_experiment.md'
remove_date: '2022-04-13'
---
This document was moved to [another location](gitlab_experiment.md).
<!-- This redirect file can be deleted after <2022-04-13>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -117,7 +117,7 @@ the model code:
```ruby
class Packages::PackageFile < ApplicationRecord
include ::Gitlab::Geo::ReplicableModel
include ::Geo::ReplicableModel
with_replicator Geo::PackageFileReplicator
end

View file

@ -37550,6 +37550,9 @@ msgstr ""
msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope youre enjoying the features of GitLab %{planName}. To keep those features after your trial ends, youll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)"
msgstr ""
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
msgstr ""
msgid "Trial|Company name"
msgstr ""
@ -37565,15 +37568,9 @@ msgstr ""
msgid "Trial|Dismiss"
msgstr ""
msgid "Trial|First name"
msgstr ""
msgid "Trial|GitLab Ultimate trial (optional)"
msgstr ""
msgid "Trial|Last name"
msgstr ""
msgid "Trial|Number of employees"
msgstr ""

View file

@ -9,7 +9,7 @@ RSpec.describe 'GraphiQL' do
end
it 'has the correct graphQLEndpoint' do
expect(page.body).to include('var graphQLEndpoint = "/api/graphql";')
expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/api/graphql"')
end
end
@ -26,7 +26,7 @@ RSpec.describe 'GraphiQL' do
end
it 'has the correct graphQLEndpoint' do
expect(page.body).to include('var graphQLEndpoint = "/gitlab/root/api/graphql";')
expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/gitlab/root/api/graphql"')
end
end
end

View file

@ -135,9 +135,6 @@ describe('~/google_tag_manager/index', () => {
describe.each([
createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
createTestCase(trackSaasTrialSubmit, {
forms: [{ id: 'new_trial', expectation: { event: 'saasTrialSubmit' } }],
}),
createTestCase(trackSaasTrialSkip, {
links: [{ cls: 'js-skip-trial', expectation: { event: 'saasTrialSkip' } }],
}),
@ -202,6 +199,18 @@ describe('~/google_tag_manager/index', () => {
});
});
describe('No listener events', () => {
it('when trackSaasTrialSubmit is invoked', () => {
expect(spy).not.toHaveBeenCalled();
trackSaasTrialSubmit();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
expect(logError).not.toHaveBeenCalled();
});
});
describe.each([
{ dataLayer: null },
{ gon: { features: null } },

View file

@ -85,19 +85,7 @@ describe('AdminRunnersApp', () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
mockRunnersCountQuery = jest.fn().mockImplementation(({ type }) => {
const mockResponse = {
[INSTANCE_TYPE]: 3,
[GROUP_TYPE]: 2,
[PROJECT_TYPE]: 1,
};
if (mockResponse[type]) {
return Promise.resolve({
data: { runners: { count: mockResponse[type] } },
});
}
return Promise.resolve(runnersCountData);
});
mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData);
createComponent();
await waitForPromises();
});
@ -107,13 +95,59 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
it('shows the runner tabs with a runner count', async () => {
createComponent({ mountFn: mount });
it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
switch (type) {
case INSTANCE_TYPE:
count = 3;
break;
case GROUP_TYPE:
count = 2;
break;
case PROJECT_TYPE:
count = 1;
break;
default:
count = 6;
break;
}
return Promise.resolve({ data: { runners: { count } } });
});
createComponent({ mountFn: mount });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All ${runnersCountData.data.runners.count} Instance 3 Group 2 Project 1`,
`All 6 Instance 3 Group 2 Project 1`,
);
});
it('shows the runner tabs with a formatted runner count', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
switch (type) {
case INSTANCE_TYPE:
count = 3000;
break;
case GROUP_TYPE:
count = 2000;
break;
case PROJECT_TYPE:
count = 1000;
break;
default:
count = 6000;
break;
}
return Promise.resolve({ data: { runners: { count } } });
});
createComponent({ mountFn: mount });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All 6,000 Instance 3,000 Group 2,000 Project 1,000`,
);
});

View file

@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
@ -15,6 +16,7 @@ Vue.use(VueApollo);
const WORK_ITEM_ID = '1';
describe('Work items root component', () => {
const mockUpdatedTitle = 'Updated title';
let wrapper;
let fakeApollo;
@ -53,7 +55,6 @@ describe('Work items root component', () => {
it('updates the title when it is edited', async () => {
createComponent();
jest.spyOn(wrapper.vm.$apollo, 'mutate');
const mockUpdatedTitle = 'Updated title';
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
@ -91,4 +92,32 @@ describe('Work items root component', () => {
expect(findTitle().exists()).toBe(false);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
createComponent();
});
afterEach(() => {
unmockTracking();
});
it('tracks item title updates', async () => {
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
await waitForPromises();
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, {
action: 'updated_title',
category: 'workItems:show',
label: 'item_title',
property: '[type_work_item]',
});
});
});
});

View file

@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe ProtectableDropdown do
subject(:dropdown) { described_class.new(project, ref_type) }
let(:project) { create(:project, :repository) }
let(:subject) { described_class.new(project, :branches) }
describe 'initialize' do
it 'raises ArgumentError for invalid ref type' do
@ -13,34 +14,75 @@ RSpec.describe ProtectableDropdown do
end
end
describe '#protectable_ref_names' do
shared_examples 'protectable_ref_names' do
context 'when project repository is not empty' do
before do
create(:protected_branch, project: project, name: 'master')
end
it 'includes elements matching a protected ref wildcard' do
is_expected.to include(matching_ref)
it { expect(subject.protectable_ref_names).to include('feature') }
it { expect(subject.protectable_ref_names).not_to include('master') }
factory = ref_type == :branches ? :protected_branch : :protected_tag
it "includes branches matching a protected branch wildcard" do
expect(subject.protectable_ref_names).to include('feature')
create(factory, name: "#{matching_ref[0]}*", project: project)
create(:protected_branch, name: 'feat*', project: project)
subject = described_class.new(project.reload, ref_type)
subject = described_class.new(project.reload, :branches)
expect(subject.protectable_ref_names).to include('feature')
expect(subject.protectable_ref_names).to include(matching_ref)
end
end
context 'when project repository is empty' do
let(:project) { create(:project) }
it "returns empty list" do
subject = described_class.new(project, :branches)
expect(subject.protectable_ref_names).to be_empty
it 'returns empty list' do
is_expected.to be_empty
end
end
end
describe '#protectable_ref_names' do
subject { dropdown.protectable_ref_names }
context 'for branches' do
let(:ref_type) { :branches }
let(:matching_ref) { 'feature' }
before do
create(:protected_branch, project: project, name: 'master')
end
it { is_expected.to include(matching_ref) }
it { is_expected.not_to include('master') }
it_behaves_like 'protectable_ref_names'
end
context 'for tags' do
let(:ref_type) { :tags }
let(:matching_ref) { 'v1.0.0' }
before do
create(:protected_tag, project: project, name: 'v1.1.0')
end
it { is_expected.to include(matching_ref) }
it { is_expected.not_to include('v1.1.0') }
it_behaves_like 'protectable_ref_names'
end
end
describe '#hash' do
subject { dropdown.hash }
context 'for branches' do
let(:ref_type) { :branches }
it { is_expected.to include(id: 'feature', text: 'feature', title: 'feature') }
end
context 'for tags' do
let(:ref_type) { :tags }
it { is_expected.to include(id: 'v1.0.0', text: 'v1.0.0', title: 'v1.0.0') }
end
end
end

View file

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RefMatcher do
subject(:ref_matcher) { described_class.new(ref_pattern) }
let(:ref_pattern) { 'v1.0' }
shared_examples 'matching_refs' do
context 'when there is no match' do
let(:ref_pattern) { 'unknown' }
it { is_expected.to match_array([]) }
end
context 'when ref pattern is a wildcard' do
let(:ref_pattern) { 'v*' }
it { is_expected.to match_array(refs) }
end
end
describe '#matching' do
subject { ref_matcher.matching(refs) }
context 'when refs are strings' do
let(:refs) { ['v1.0', 'v1.1'] }
it { is_expected.to match_array([ref_pattern]) }
it_behaves_like 'matching_refs'
end
context 'when refs are ref objects' do
let(:matching_ref) { double('tag', name: 'v1.0') }
let(:not_matching_ref) { double('tag', name: 'v1.1') }
let(:refs) { [matching_ref, not_matching_ref] }
it { is_expected.to match_array([matching_ref]) }
it_behaves_like 'matching_refs'
end
end
describe '#matches?' do
subject { ref_matcher.matches?(ref_name) }
let(:ref_name) { 'v1.0' }
it { is_expected.to be_truthy }
context 'when ref_name is empty' do
let(:ref_name) { '' }
it { is_expected.to be_falsey }
end
context 'when ref pattern matches wildcard' do
let(:ref_pattern) { 'v*' }
it { is_expected.to be_truthy }
end
context 'when ref pattern does not match wildcard' do
let(:ref_pattern) { 'v2.*' }
it { is_expected.to be_falsey }
end
end
describe '#wildcard?' do
subject { ref_matcher.wildcard? }
it { is_expected.to be_falsey }
context 'when pattern is a wildcard' do
let(:ref_pattern) { 'v*' }
it { is_expected.to be_truthy }
end
end
end