Commit d24e999d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 3abef158 16d2c280
<script> <script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin'; import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
...@@ -13,12 +12,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -13,12 +12,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
GlButton, GlButton,
GlEmptyState,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlModal, GlModal,
PipelinesTableComponent, PipelinesTableComponent,
TablePagination, TablePagination,
SvgBlankState,
}, },
mixins: [PipelinesMixin, glFeatureFlagMixin()], mixins: [PipelinesMixin, glFeatureFlagMixin()],
props: { props: {
...@@ -183,12 +182,12 @@ export default { ...@@ -183,12 +182,12 @@ export default {
class="prepend-top-20" class="prepend-top-20"
/> />
<svg-blank-state <gl-empty-state
v-else-if="shouldRenderErrorState" v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
:message=" :title="
s__(`Pipelines|There was an error fetching the pipelines. s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`) Try again in a few moments or contact your support team.`)
" "
/> />
......
<script>
export default {
name: 'PipelinesSvgState',
props: {
svgPath: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="row empty-state">
<div class="col-12">
<div class="svg-content"><img :src="svgPath" /></div>
</div>
<div class="col-12 text-center">
<div class="text-content">
<h4>{{ message }}</h4>
</div>
</div>
</div>
</template>
<script> <script>
import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
...@@ -10,7 +10,6 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../ ...@@ -10,7 +10,6 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../
import PipelinesMixin from '../../mixins/pipelines_mixin'; import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service'; import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils'; import { validateParams } from '../../utils';
import SvgBlankState from './blank_state.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue'; import NavigationControls from './nav_controls.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
...@@ -19,13 +18,13 @@ import PipelinesTableComponent from './pipelines_table.vue'; ...@@ -19,13 +18,13 @@ import PipelinesTableComponent from './pipelines_table.vue';
export default { export default {
components: { components: {
EmptyState, EmptyState,
GlEmptyState,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
NavigationTabs, NavigationTabs,
NavigationControls, NavigationControls,
PipelinesFilteredSearch, PipelinesFilteredSearch,
PipelinesTableComponent, PipelinesTableComponent,
SvgBlankState,
TablePagination, TablePagination,
}, },
mixins: [PipelinesMixin], mixins: [PipelinesMixin],
...@@ -333,19 +332,19 @@ export default { ...@@ -333,19 +332,19 @@ export default {
:can-set-ci="canCreatePipeline" :can-set-ci="canCreatePipeline"
/> />
<svg-blank-state <gl-empty-state
v-else-if="stateToRender === $options.stateMap.error" v-else-if="stateToRender === $options.stateMap.error"
:svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
:message=" :title="
s__(`Pipelines|There was an error fetching the pipelines. s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`) Try again in a few moments or contact your support team.`)
" "
/> />
<svg-blank-state <gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab" v-else-if="stateToRender === $options.stateMap.emptyTab"
:svg-path="noPipelinesSvgPath" :svg-path="noPipelinesSvgPath"
:message="emptyTabMessage" :title="emptyTabMessage"
/> />
<div v-else-if="stateToRender === $options.stateMap.tableList"> <div v-else-if="stateToRender === $options.stateMap.tableList">
......
...@@ -48,8 +48,16 @@ export default { ...@@ -48,8 +48,16 @@ export default {
legacyTableMobileClass() { legacyTableMobileClass() {
return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : ''; return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : '';
}, },
singleStagePipelineManual() {
return (
this.pipeline.details.manual_actions.length > 0 && this.pipeline.details.stages.length === 1
);
},
showInProgress() { showInProgress() {
return !this.duration && !this.finishedTime; return !this.duration && !this.finishedTime && !this.singleStagePipelineManual;
},
showSkipped() {
return !this.duration && !this.finishedTime && this.singleStagePipelineManual;
}, },
}, },
}; };
...@@ -65,6 +73,11 @@ export default { ...@@ -65,6 +73,11 @@ export default {
{{ s__('Pipeline|In progress') }} {{ s__('Pipeline|In progress') }}
</span> </span>
<span v-if="showSkipped" data-testid="pipeline-skipped">
<gl-icon name="status_skipped_borderless" class="gl-mr-2" :size="16" />
{{ s__('Pipeline|Skipped') }}
</span>
<p v-if="duration" class="duration"> <p v-if="duration" class="duration">
<gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" /> <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" />
{{ durationFormatted }} {{ durationFormatted }}
......
...@@ -71,7 +71,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -71,7 +71,7 @@ class Projects::ServicesController < Projects::ApplicationController
end end
result[:data].presence || {} result[:data].presence || {}
rescue Gitlab::HTTP::BlockedUrlError => e rescue *Gitlab::HTTP::HTTP_ERRORS => e
{ error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true } { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true }
end end
......
...@@ -995,6 +995,12 @@ class Repository ...@@ -995,6 +995,12 @@ class Repository
raw_repository.search_files_by_name(query, ref) raw_repository.search_files_by_name(query, ref)
end end
def search_files_by_wildcard_path(path, ref = 'HEAD')
# We need to use RE2 to match Gitaly's regexp engine
regexp_string = RE2::Regexp.escape(path).gsub('\*', '.*?')
raw_repository.search_files_by_regexp("^#{regexp_string}$", ref)
end
def copy_gitattributes(ref) def copy_gitattributes(ref)
actual_ref = ref || root_ref actual_ref = ref || root_ref
begin begin
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- user_status_data = user_status_properties(current_user) - user_status_data = user_status_properties(current_user)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } } %header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content %a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid .container-fluid
.header-content .header-content
.title-container .title-container
......
---
title: Enable :jira_issues_show_integration feature flag by default
merge_request: 56182
author:
type: added
---
title: Remove tabindex on skip link that could negatively impact keyboard focus management
and order
merge_request: 55756
author:
type: other
---
title: Support newlines for the chatops "run" command
merge_request: 56668
author:
type: changed
---
title: Adds skipped state to duration cell for single stage manual pipelines
merge_request: 56669
author:
type: changed
---
title: Catch network errors
merge_request: 56457
author: Shubham Kumar (@imskr)
type: fixed
...@@ -8,10 +8,11 @@ product_category: collection ...@@ -8,10 +8,11 @@ product_category: collection
value_type: string value_type: string
status: data_available status: data_available
time_frame: none time_frame: none
data_source: data_source: ruby
distribution: distribution:
- ce - ce
- ee - ee
tier: tier:
- free - free
skip_validation: true - premium
- ultimate
...@@ -8121,11 +8121,11 @@ The dismissal reason of the Vulnerability. ...@@ -8121,11 +8121,11 @@ The dismissal reason of the Vulnerability.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| `ACCEPTABLE_RISK` | The likelihood of the Vulnerability occurring and its impact are deemed acceptable | | `ACCEPTABLE_RISK` | The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk. |
| `FALSE_POSITIVE` | The Vulnerability was incorrectly identified as being present | | `FALSE_POSITIVE` | An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present. |
| `MITIGATING_CONTROL` | There is a mitigating control that eliminates the Vulnerability or makes its risk acceptable | | `MITIGATING_CONTROL` | A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system. |
| `NOT_APPLICABLE` | Other reasons for dismissal | | `NOT_APPLICABLE` | The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated. |
| `USED_IN_TESTS` | The Vulnerability is used in tests and does not pose an actual risk | | `USED_IN_TESTS` | The finding is not a vulnerability because it is part of a test or is test data. |
### `VulnerabilityExternalIssueLinkExternalTracker` ### `VulnerabilityExternalIssueLinkExternalTracker`
......
This diff is collapsed.
...@@ -7158,7 +7158,7 @@ Group: `group::product intelligence` ...@@ -7158,7 +7158,7 @@ Group: `group::product intelligence`
Status: `data_available` Status: `data_available`
Tiers: `free` Tiers: `free`, `premium`, `ultimate`
### `recording_ee_finished_at` ### `recording_ee_finished_at`
......
...@@ -50,16 +50,16 @@ These notification settings apply only to you. They do not affect the notificati ...@@ -50,16 +50,16 @@ These notification settings apply only to you. They do not affect the notificati
## Global notification settings ## Global notification settings
Your **Global notification settings** are the default settings unless you select different values for a project or a group. Your **Global notification settings** are the default settings unless you select
different values for a project or a group.
- Notification email - **Notification email**: The email address your notifications are sent to.
- This is the email address your notifications are sent to. - **Global notification level**: The default [notification level](#notification-levels)
- Global notification level which applies to all your notifications.
- This is the default [notification level](#notification-levels) which applies to all your notifications. - **Receive product marketing emails**: Select this check box to receive periodic
- Receive product marketing emails emails about GitLab features.
- Check this checkbox if you want to receive periodic emails related to GitLab features. - **Receive notifications about your own activity**: Select this check box to receive
- Receive notifications about your own activity. notifications about your own activity. Not selected by default.
- Check this checkbox if you want to receive notification about your own activity. Default: Not checked.
### Notification scope ### Notification scope
...@@ -67,16 +67,16 @@ You can tune the scope of your notifications by selecting different notification ...@@ -67,16 +67,16 @@ You can tune the scope of your notifications by selecting different notification
Notification scope is applied in order of precedence (highest to lowest): Notification scope is applied in order of precedence (highest to lowest):
- Project - **Project**: For each project, you can select a notification level. Your project
- For each project, you can select a notification level. Your project setting overrides the group setting. setting overrides the group setting.
- Group - **Group**: For each group, you can select a notification level. Your group setting
- For each group, you can select a notification level. Your group setting overrides your default setting. overrides your default setting.
- Global (default) - **Global (default)**: Your global, or _default_, notification level applies if you
- Your global, or _default_, notification level applies if you have not selected a notification level for the project or group in which the activity occurred. have not selected a notification level for the project or group in which the activity occurred.
#### Project notifications #### Project notifications
You can select a notification level for each project. This can be useful if you need to closely monitor activity in select projects. You can select a notification level for each project to help you closely monitor activity in select projects.
![notification settings](img/notification_project_settings_v12_8.png) ![notification settings](img/notification_project_settings_v12_8.png)
......
...@@ -242,19 +242,38 @@ with a link to the commit that resolved the issue. ...@@ -242,19 +242,38 @@ with a link to the commit that resolved the issue.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
You can browse and search issues from a selected Jira project directly in GitLab. This requires [configuration](#configure-gitlab) in GitLab by an administrator. You can browse, search, and view issues from a selected Jira project directly in GitLab,
if your GitLab administrator [has configured it](#configure-gitlab):
![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png) 1. In the left navigation bar, go to **Jira > Issues list**.
1. The issue list sorts by **Created date** by default, with the newest issues listed at the top:
From the **Jira Issues** menu, click **Issues List**. The issue list defaults to sort by **Created date**, with the newest issues listed at the top. You can change this to **Last updated**. ![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png)
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html). 1. To display the most recently updated issues first, click **Last updated**.
1. In GitLab versions 13.10 and later, you can view [individual Jira issues](#view-a-jira-issue).
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html):
- The **Open** tab displays all issues with a Jira status in any category other than Done. - The **Open** tab displays all issues with a Jira status in any category other than Done.
- The **Closed** tab displays all issues with a Jira status categorized as Done. - The **Closed** tab displays all issues with a Jira status categorized as Done.
- The **All** tab displays all issues of any status. - The **All** tab displays all issues of any status.
Click an issue title to open its original Jira issue page for full details. #### View a Jira issue
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299832) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-jira-issue-detail-view). **(PREMIUM)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
When viewing the [Jira issues list](#view-jira-issues), select an issue from the
list to open it in GitLab:
![Jira issue detail view](img/jira/jira_issue_detail_view_v13.10.png)
#### Search and filter the issues list #### Search and filter the issues list
...@@ -304,3 +323,22 @@ which may lead to a `401 unauthorized` error when testing your Jira integration. ...@@ -304,3 +323,22 @@ which may lead to a `401 unauthorized` error when testing your Jira integration.
If CAPTCHA has been triggered, you can't use Jira's REST API to If CAPTCHA has been triggered, you can't use Jira's REST API to
authenticate with the Jira site. You need to log in to your Jira instance authenticate with the Jira site. You need to log in to your Jira instance
and complete the CAPTCHA. and complete the CAPTCHA.
## Enable or disable Jira issue detail view
Jira issue detail view is under development but ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:jira_issues_show_integration)
```
To disable it:
```ruby
Feature.disable(:jira_issues_show_integration)
```
...@@ -4,7 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -4,7 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { SAVE_ERROR } from '../constants'; import { SAVE_ERROR } from '../constants';
import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql'; import createComplianceFrameworkMutation from '../graphql/queries/create_compliance_framework.mutation.graphql';
import { initialiseFormData } from '../utils'; import { getSubmissionParams, initialiseFormData } from '../utils';
import FormStatus from './form_status.vue'; import FormStatus from './form_status.vue';
import SharedForm from './shared_form.vue'; import SharedForm from './shared_form.vue';
...@@ -51,18 +51,16 @@ export default { ...@@ -51,18 +51,16 @@ export default {
this.errorMessage = ''; this.errorMessage = '';
try { try {
const { name, description, pipelineConfigurationFullPath, color } = this.formData; const params = getSubmissionParams(
this.formData,
this.pipelineConfigurationFullPathEnabled,
);
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: createComplianceFrameworkMutation, mutation: createComplianceFrameworkMutation,
variables: { variables: {
input: { input: {
namespacePath: this.groupPath, namespacePath: this.groupPath,
params: { params,
name,
description,
pipelineConfigurationFullPath,
color,
},
}, },
}, },
}); });
......
...@@ -7,7 +7,7 @@ import { __ } from '~/locale'; ...@@ -7,7 +7,7 @@ import { __ } from '~/locale';
import { FETCH_ERROR, SAVE_ERROR } from '../constants'; import { FETCH_ERROR, SAVE_ERROR } from '../constants';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql'; import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql'; import updateComplianceFrameworkMutation from '../graphql/queries/update_compliance_framework.mutation.graphql';
import { initialiseFormData } from '../utils'; import { getSubmissionParams, initialiseFormData } from '../utils';
import FormStatus from './form_status.vue'; import FormStatus from './form_status.vue';
import SharedForm from './shared_form.vue'; import SharedForm from './shared_form.vue';
...@@ -114,18 +114,16 @@ export default { ...@@ -114,18 +114,16 @@ export default {
this.saveErrorMessage = ''; this.saveErrorMessage = '';
try { try {
const { name, description, pipelineConfigurationFullPath, color } = this.formData; const params = getSubmissionParams(
this.formData,
this.pipelineConfigurationFullPathEnabled,
);
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: updateComplianceFrameworkMutation, mutation: updateComplianceFrameworkMutation,
variables: { variables: {
input: { input: {
id: this.graphqlId, id: this.graphqlId,
params: { params,
name,
description,
pipelineConfigurationFullPath,
color,
},
}, },
}, },
}); });
......
...@@ -18,6 +18,16 @@ export const initialiseFormData = () => ({ ...@@ -18,6 +18,16 @@ export const initialiseFormData = () => ({
color: null, color: null,
}); });
export const getSubmissionParams = (formData, pipelineConfigurationFullPathEnabled) => {
const params = { ...formData };
if (!pipelineConfigurationFullPathEnabled) {
delete params.pipelineConfigurationFullPath;
}
return params;
};
export const getPipelineConfigurationPathParts = (path) => { export const getPipelineConfigurationPathParts = (path) => {
const [, file, group, project] = path.match(PIPELINE_CONFIGURATION_PATH_FORMAT) || []; const [, file, group, project] = path.match(PIPELINE_CONFIGURATION_PATH_FORMAT) || [];
......
...@@ -9,11 +9,11 @@ module Vulnerabilities ...@@ -9,11 +9,11 @@ module Vulnerabilities
description 'The dismissal reason of the Vulnerability' description 'The dismissal reason of the Vulnerability'
define do define do
acceptable_risk value: 0, description: 'The likelihood of the Vulnerability occurring and its impact are deemed acceptable' acceptable_risk value: 0, description: _('The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk.')
false_positive value: 1, description: 'The Vulnerability was incorrectly identified as being present' false_positive value: 1, description: _('An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present.')
mitigating_control value: 2, description: 'There is a mitigating control that eliminates the Vulnerability or makes its risk acceptable' mitigating_control value: 2, description: _('A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system.')
used_in_tests value: 3, description: 'The Vulnerability is used in tests and does not pose an actual risk' used_in_tests value: 3, description: _('The finding is not a vulnerability because it is part of a test or is test data.')
not_applicable value: 4, description: 'Other reasons for dismissal' not_applicable value: 4, description: _('The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated.')
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module VulnerabilitiesHelper module VulnerabilitiesHelper
FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details].freeze FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback].freeze
def vulnerability_details_json(vulnerability, pipeline) def vulnerability_details_json(vulnerability, pipeline)
vulnerability_details(vulnerability, pipeline).to_json vulnerability_details(vulnerability, pipeline).to_json
......
...@@ -49,6 +49,11 @@ class Vulnerabilities::FeedbackEntity < Grape::Entity ...@@ -49,6 +49,11 @@ class Vulnerabilities::FeedbackEntity < Grape::Entity
end end
expose :project_fingerprint expose :project_fingerprint
expose :dismissal_reason
expose :dismissal_descriptions do |feedback|
Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] }
end
alias_method :feedback, :object alias_method :feedback, :object
private private
......
---
title: Expose dismissal reason and dismissal descriptions in Vulnerability details
view
merge_request: 55525
author:
type: added
...@@ -20,6 +20,7 @@ FactoryBot.define do ...@@ -20,6 +20,7 @@ FactoryBot.define do
trait :dismissal do trait :dismissal do
feedback_type { 'dismissal' } feedback_type { 'dismissal' }
dismissal_reason { 'acceptable_risk' }
end end
trait :comment do trait :comment do
......
...@@ -37,7 +37,11 @@ ...@@ -37,7 +37,11 @@
"project_fingerprint": { "type": "string" }, "project_fingerprint": { "type": "string" },
"branch": { "type": ["string", "null"] }, "branch": { "type": ["string", "null"] },
"destroy_vulnerability_feedback_dismissal_path": { "type": "string" }, "destroy_vulnerability_feedback_dismissal_path": { "type": "string" },
"finding_uuid": { "type": ["string", "null"] } "finding_uuid": { "type": ["string", "null"] },
"dismissal_reason": { "type": ["string", "null"] },
"dismissal_descriptions": {
"type": {"string": "string"}
}
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -41,6 +41,43 @@ describe('Utils', () => { ...@@ -41,6 +41,43 @@ describe('Utils', () => {
}); });
}); });
describe('getSubmissionParams', () => {
const baseFormData = {
name: 'a',
description: 'b',
color: '#000',
};
it.each([true, false])(
'should return the initial object when pipelineConfigurationFullPath is undefined and pipelineConfigurationFullPathEnabled is %s',
(enabled) => {
expect(Utils.getSubmissionParams(baseFormData, enabled)).toStrictEqual(baseFormData);
},
);
it.each`
pipelineConfigurationFullPath | pipelineConfigurationFullPathEnabled
${'a/b'} | ${true}
${null} | ${true}
${'a/b'} | ${false}
${null} | ${false}
`(
'should return the correct object when pipelineConfigurationFullPathEnabled is $pipelineConfigurationFullPathEnabled',
({ pipelineConfigurationFullPath, pipelineConfigurationFullPathEnabled }) => {
const formData = Utils.getSubmissionParams(
{ ...baseFormData, pipelineConfigurationFullPath },
pipelineConfigurationFullPathEnabled,
);
if (pipelineConfigurationFullPathEnabled) {
expect(formData).toStrictEqual({ ...baseFormData, pipelineConfigurationFullPath });
} else {
expect(formData).toStrictEqual(baseFormData);
}
},
);
});
describe('getPipelineConfigurationPathParts', () => { describe('getPipelineConfigurationPathParts', () => {
it.each` it.each`
path | parts path | parts
......
...@@ -4,9 +4,9 @@ require 'spec_helper' ...@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe VulnerabilitiesHelper do RSpec.describe VulnerabilitiesHelper do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
let(:pipeline) { create(:ci_pipeline, :success, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) } let_it_be(:finding) { create(:vulnerabilities_finding, pipelines: [pipeline], project: project, severity: :high) }
let(:vulnerability) { create(:vulnerability, title: "My vulnerability", project: project, findings: [finding]) } let(:vulnerability) { create(:vulnerability, title: "My vulnerability", project: project, findings: [finding]) }
before do before do
...@@ -43,7 +43,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -43,7 +43,7 @@ RSpec.describe VulnerabilitiesHelper do
:details) :details)
end end
let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details] } let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback] }
before do before do
vulnerability_serializer_stub = instance_double("VulnerabilitySerializer") vulnerability_serializer_stub = instance_double("VulnerabilitySerializer")
...@@ -270,7 +270,8 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -270,7 +270,8 @@ RSpec.describe VulnerabilitiesHelper do
assets: kind_of(Array), assets: kind_of(Array),
supporting_messages: kind_of(Array), supporting_messages: kind_of(Array),
uuid: kind_of(String), uuid: kind_of(String),
details: kind_of(Hash) details: kind_of(Hash),
dismissal_feedback: anything
) )
expect(subject[:location]['blob_path']).to match(kind_of(String)) expect(subject[:location]['blob_path']).to match(kind_of(String))
...@@ -286,6 +287,17 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -286,6 +287,17 @@ RSpec.describe VulnerabilitiesHelper do
expect(subject[:location]).not_to have_key('blob_path') expect(subject[:location]).not_to have_key('blob_path')
end end
end end
context 'with existing dismissal feedback' do
let_it_be(:feedback) { create(:vulnerability_feedback, :comment, :dismissal, project: project, pipeline: pipeline, project_fingerprint: finding.project_fingerprint) }
it 'returns dismissal feedback information', :aggregate_failures do
dismissal_feedback = subject[:dismissal_feedback]
expect(dismissal_feedback[:dismissal_reason]).to eq(feedback.dismissal_reason)
expect(dismissal_feedback[:dismissal_descriptions]).to eq(Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] })
expect(dismissal_feedback[:comment_details][:comment]).to eq(feedback.comment)
end
end
end end
describe '#vulnerability_scan_data?' do describe '#vulnerability_scan_data?' do
......
...@@ -178,4 +178,28 @@ RSpec.describe Vulnerabilities::FeedbackEntity do ...@@ -178,4 +178,28 @@ RSpec.describe Vulnerabilities::FeedbackEntity do
expect(subject[:finding_uuid]).to eq(finding.uuid) expect(subject[:finding_uuid]).to eq(finding.uuid)
end end
end end
context 'when dismissal_reason is not present' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :issue, project: project) }
it "returns nil" do
expect(subject[:dismissal_reason]).to be_nil
end
end
context 'when dismissal_reason is present' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :dismissal, project: project) }
it 'exposes dismissal_reason' do
expect(subject[:dismissal_reason]).to eq(feedback.dismissal_reason)
end
end
context 'when dismissal descriptions are available' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :dismissal, project: project) }
it 'exposes dismissal_descriptions' do
expect(subject[:dismissal_descriptions]).to eq(Vulnerabilities::DismissalReasonEnum.definition.transform_values { |v| v[:description] })
end
end
end end
...@@ -1017,6 +1017,10 @@ module Gitlab ...@@ -1017,6 +1017,10 @@ module Gitlab
gitaly_repository_client.search_files_by_name(ref, safe_query) gitaly_repository_client.search_files_by_name(ref, safe_query)
end end
def search_files_by_regexp(filter, ref = 'HEAD')
gitaly_repository_client.search_files_by_regexp(ref, filter)
end
def find_commits_by_message(query, ref, path, limit, offset) def find_commits_by_message(query, ref, path, limit, offset)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_commit_client gitaly_commit_client
......
...@@ -339,6 +339,11 @@ module Gitlab ...@@ -339,6 +339,11 @@ module Gitlab
search_results_from_response(response, options) search_results_from_response(response, options)
end end
def search_files_by_regexp(ref, filter)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter)
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
def disconnect_alternates def disconnect_alternates
request = Gitaly::DisconnectGitAlternatesRequest.new( request = Gitaly::DisconnectGitAlternatesRequest.new(
repository: @gitaly_repo repository: @gitaly_repo
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
# Slash command for triggering chatops jobs. # Slash command for triggering chatops jobs.
class Run < BaseCommand class Run < BaseCommand
def self.match(text) def self.match(text)
/\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/.match(text) /\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/m.match(text)
end end
def self.help_message def self.help_message
......
...@@ -1368,6 +1368,9 @@ msgstr "" ...@@ -1368,6 +1368,9 @@ msgstr ""
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies." msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr "" msgstr ""
msgid "A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system."
msgstr ""
msgid "A member of the abuse team will review your report as soon as possible." msgid "A member of the abuse team will review your report as soon as possible."
msgstr "" msgstr ""
...@@ -3262,6 +3265,9 @@ msgstr "" ...@@ -3262,6 +3265,9 @@ msgstr ""
msgid "An error has occurred" msgid "An error has occurred"
msgstr "" msgstr ""
msgid "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present."
msgstr ""
msgid "An error occurred adding a draft to the thread." msgid "An error occurred adding a draft to the thread."
msgstr "" msgstr ""
...@@ -29992,6 +29998,9 @@ msgstr "" ...@@ -29992,6 +29998,9 @@ msgstr ""
msgid "The file name should have a .yml extension" msgid "The file name should have a .yml extension"
msgstr "" msgstr ""
msgid "The finding is not a vulnerability because it is part of a test or is test data."
msgstr ""
msgid "The following %{user} can also merge into this branch: %{branch}" msgid "The following %{user} can also merge into this branch: %{branch}"
msgstr "" msgstr ""
...@@ -30294,6 +30303,12 @@ msgstr "" ...@@ -30294,6 +30303,12 @@ msgstr ""
msgid "The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax." msgid "The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax."
msgstr "" msgstr ""
msgid "The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk."
msgstr ""
msgid "The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated."
msgstr ""
msgid "The vulnerability is no longer detected. Verify the vulnerability has been fixed or removed before changing its status." msgid "The vulnerability is no longer detected. Verify the vulnerability has been fixed or removed before changing its status."
msgstr "" msgstr ""
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::ServicesController do RSpec.describe Projects::ServicesController do
include JiraServiceHelper include JiraServiceHelper
include AfterNextHelpers
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -13,7 +14,6 @@ RSpec.describe Projects::ServicesController do ...@@ -13,7 +14,6 @@ RSpec.describe Projects::ServicesController do
before do before do
sign_in(user) sign_in(user)
project.add_maintainer(user) project.add_maintainer(user)
allow(Gitlab::UrlBlocker).to receive(:validate!).and_return([URI.parse('http://example.com'), nil])
end end
describe '#test' do describe '#test' do
...@@ -114,7 +114,7 @@ RSpec.describe Projects::ServicesController do ...@@ -114,7 +114,7 @@ RSpec.describe Projects::ServicesController do
end end
context 'failure' do context 'failure' do
it 'returns success status code and the error message' do it 'returns an error response when the integration test fails' do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo') stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404) .to_return(status: 404)
...@@ -128,6 +128,36 @@ RSpec.describe Projects::ServicesController do ...@@ -128,6 +128,36 @@ RSpec.describe Projects::ServicesController do
'test_failed' => true 'test_failed' => true
) )
end end
context 'with the Slack integration' do
let_it_be(:service) { build(:slack_service) }
it 'returns an error response when the URL is blocked' do
put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed",
'test_failed' => true
)
end
it 'returns an error response when a network exception is raised' do
expect_next(SlackService).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => 'Connection refused',
'test_failed' => true
)
end
end
end end
end end
......
import { getByText } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
describe('Pipelines Blank State', () => {
const wrapper = mount(BlankState, {
propsData: {
svgPath: 'foo',
message: 'Blank State',
},
});
it('should render svg', () => {
expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo');
});
it('should render message', () => {
expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy();
});
});
import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
...@@ -8,8 +8,6 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -8,8 +8,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
...@@ -58,11 +56,10 @@ describe('Pipelines', () => { ...@@ -58,11 +56,10 @@ describe('Pipelines', () => {
}; };
const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findNavigationTabs = () => wrapper.findComponent(NavigationTabs); const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
const findNavigationControls = () => wrapper.findComponent(NavigationControls); const findNavigationControls = () => wrapper.findComponent(NavigationControls);
const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent); const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
const findEmptyState = () => wrapper.findComponent(EmptyState);
const findBlankState = () => wrapper.findComponent(BlankState);
const findTablePagination = () => wrapper.findComponent(TablePagination); const findTablePagination = () => wrapper.findComponent(TablePagination);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`); const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
...@@ -268,7 +265,7 @@ describe('Pipelines', () => { ...@@ -268,7 +265,7 @@ describe('Pipelines', () => {
}); });
it('should filter pipelines', async () => { it('should filter pipelines', async () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.'); expect(findEmptyState().text()).toBe('There are currently no pipelines.');
}); });
it('should update browser bar', () => { it('should update browser bar', () => {
...@@ -515,7 +512,7 @@ describe('Pipelines', () => { ...@@ -515,7 +512,7 @@ describe('Pipelines', () => {
}); });
it('renders empty state', () => { it('renders empty state', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.'); expect(findEmptyState().text()).toBe('There are currently no pipelines.');
}); });
it('renders tab empty state finished scope', async () => { it('renders tab empty state finished scope', async () => {
...@@ -528,7 +525,7 @@ describe('Pipelines', () => { ...@@ -528,7 +525,7 @@ describe('Pipelines', () => {
await waitForPromises(); await waitForPromises();
expect(findBlankState().text()).toBe('There are currently no finished pipelines.'); expect(findEmptyState().text()).toBe('There are currently no finished pipelines.');
}); });
}); });
...@@ -599,7 +596,7 @@ describe('Pipelines', () => { ...@@ -599,7 +596,7 @@ describe('Pipelines', () => {
}); });
it('renders empty state', () => { it('renders empty state', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.'); expect(findEmptyState().text()).toBe('There are currently no pipelines.');
}); });
}); });
}); });
...@@ -688,7 +685,7 @@ describe('Pipelines', () => { ...@@ -688,7 +685,7 @@ describe('Pipelines', () => {
}); });
it('shows error state', () => { it('shows error state', () => {
expect(findBlankState().text()).toBe( expect(findEmptyState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
); );
}); });
...@@ -713,7 +710,7 @@ describe('Pipelines', () => { ...@@ -713,7 +710,7 @@ describe('Pipelines', () => {
}); });
it('shows error state', () => { it('shows error state', () => {
expect(findBlankState().text()).toBe( expect(findEmptyState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
); );
}); });
......
...@@ -5,6 +5,41 @@ import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue'; ...@@ -5,6 +5,41 @@ import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => { describe('Timeago component', () => {
let wrapper; let wrapper;
const mutlipleStages = {
manual_actions: [
{
name: 'deploy_job',
path: '/root/one-stage-manual/-/jobs/1930/play',
playable: true,
scheduled: false,
},
],
stages: [
{
name: 'deploy',
},
{
name: 'qa',
},
],
};
const singleStageManual = {
manual_actions: [
{
name: 'deploy_job',
path: '/root/one-stage-manual/-/jobs/1930/play',
playable: true,
scheduled: false,
},
],
stages: [
{
name: 'deploy',
},
],
};
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(TimeAgo, { wrapper = shallowMount(TimeAgo, {
propsData: { propsData: {
...@@ -30,6 +65,7 @@ describe('Timeago component', () => { ...@@ -30,6 +65,7 @@ describe('Timeago component', () => {
const duration = () => wrapper.find('.duration'); const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at'); const finishedAt = () => wrapper.find('.finished-at');
const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]'); const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]');
const findSkipped = () => wrapper.find('[data-testid="pipeline-skipped"]');
describe('with duration', () => { describe('with duration', () => {
beforeEach(() => { beforeEach(() => {
...@@ -46,7 +82,7 @@ describe('Timeago component', () => { ...@@ -46,7 +82,7 @@ describe('Timeago component', () => {
describe('without duration', () => { describe('without duration', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ duration: 0, finished_at: '' }); createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
}); });
it('should not render duration and timer svg', () => { it('should not render duration and timer svg', () => {
...@@ -71,7 +107,7 @@ describe('Timeago component', () => { ...@@ -71,7 +107,7 @@ describe('Timeago component', () => {
describe('without finishedTime', () => { describe('without finishedTime', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ duration: 0, finished_at: '' }); createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
}); });
it('should not render time and calendar icon', () => { it('should not render time and calendar icon', () => {
...@@ -89,9 +125,36 @@ describe('Timeago component', () => { ...@@ -89,9 +125,36 @@ describe('Timeago component', () => {
`( `(
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime', 'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => { ({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({ duration: durationTime, finished_at: finishedAtTime }); createComponent({
...mutlipleStages,
duration: durationTime,
finished_at: finishedAtTime,
});
expect(findInProgress().exists()).toBe(shouldShow); expect(findInProgress().exists()).toBe(shouldShow);
expect(findSkipped().exists()).toBe(false);
},
);
});
describe('skipped', () => {
it.each`
durationTime | finishedAtTime | shouldShow
${10} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${10} | ${''} | ${false}
${0} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${0} | ${''} | ${true}
`(
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({
...singleStageManual,
duration: durationTime,
finished_at: finishedAtTime,
});
expect(findSkipped().exists()).toBe(shouldShow);
expect(findInProgress().exists()).toBe(false);
}, },
); );
}); });
......
...@@ -564,6 +564,41 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ...@@ -564,6 +564,41 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end end
end end
describe '#search_files_by_regexp' do
let(:ref) { 'master' }
subject(:result) { mutable_repository.search_files_by_regexp(filter, ref) }
context 'when sending a valid regexp' do
let(:filter) { 'files\/.*\/.*\.rb' }
it 'returns matched files' do
expect(result).to contain_exactly('files/links/regex.rb',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/ruby/version_info.rb')
end
end
context 'when sending an ivalid regexp' do
let(:filter) { '*.rb' }
it 'raises error' do
expect { result }.to raise_error(GRPC::InvalidArgument,
/missing argument to repetition operator: `*`/)
end
end
context "when the ref doesn't exist" do
let(:filter) { 'files\/.*\/.*\.rb' }
let(:ref) { 'non-existing-branch' }
it 'returns an empty array' do
expect(result).to eq([])
end
end
end
describe '#find_remote_root_ref' do describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService) expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
......
...@@ -246,6 +246,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do ...@@ -246,6 +246,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end end
end end
describe '#search_files_by_regexp' do
subject(:result) { client.search_files_by_regexp('master', '.*') }
before do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:search_files_by_name)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([double(files: ['file1.txt']), double(files: ['file2.txt'])])
end
it 'sends a search_files_by_name message and returns a flatten array' do
expect(result).to contain_exactly('file1.txt', 'file2.txt')
end
end
describe '#disconnect_alternates' do describe '#disconnect_alternates' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repository) { project.repository } let(:repository) { project.repository }
......
...@@ -3,6 +3,26 @@ ...@@ -3,6 +3,26 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::SlashCommands::Run do RSpec.describe Gitlab::SlashCommands::Run do
describe '.match' do
it 'returns true for a run command' do
expect(described_class.match('run foo')).to be_an_instance_of(MatchData)
end
it 'returns true for a run command with arguments' do
expect(described_class.match('run foo bar baz'))
.to be_an_instance_of(MatchData)
end
it 'returns true for a command containing newlines' do
expect(described_class.match("run foo\nbar\nbaz"))
.to be_an_instance_of(MatchData)
end
it 'returns false for an unrelated command' do
expect(described_class.match('foo bar')).to be_nil
end
end
describe '.available?' do describe '.available?' do
it 'returns true when builds are enabled for the project' do it 'returns true when builds are enabled for the project' do
project = double(:project, builds_enabled?: true) project = double(:project, builds_enabled?: true)
......
...@@ -977,6 +977,57 @@ RSpec.describe Repository do ...@@ -977,6 +977,57 @@ RSpec.describe Repository do
end end
end end
describe '#search_files_by_wildcard_path' do
let(:ref) { 'master' }
subject(:result) { repository.search_files_by_wildcard_path(path, ref) }
context 'when specifying a normal path' do
let(:path) { 'files/images/logo-black.png' }
it 'returns the path' do
expect(result).to eq(['files/images/logo-black.png'])
end
end
context 'when specifying a path with wildcard' do
let(:path) { 'files/*/*.png' }
it 'returns all files matching the path' do
expect(result).to contain_exactly('files/images/logo-black.png',
'files/images/logo-white.png')
end
end
context 'when specifying an extension with wildcard' do
let(:path) { '*.rb' }
it 'returns all files matching the extension' do
expect(result).to contain_exactly('encoding/russian.rb',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
'files/ruby/version_info.rb')
end
end
context 'when sending regexp' do
let(:path) { '.*\.rb' }
it 'ignores the regexp and returns an empty array' do
expect(result).to eq([])
end
end
context 'when sending another ref' do
let(:path) { 'files' }
let(:ref) { 'other-branch' }
it 'returns an empty array' do
expect(result).to eq([])
end
end
end
describe '#async_remove_remote' do describe '#async_remove_remote' do
before do before do
masterrev = repository.find_branch('master').dereferenced_target masterrev = repository.find_branch('master').dereferenced_target
......
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