Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
49a897eff9
commit
a0158b1a9c
48 changed files with 712 additions and 632 deletions
|
@ -1,5 +1,3 @@
|
|||
import Shortcuts from './shortcuts/shortcuts';
|
||||
|
||||
export default function initPageShortcuts() {
|
||||
const { page } = document.body.dataset;
|
||||
const pagesWithCustomShortcuts = [
|
||||
|
@ -29,7 +27,9 @@ export default function initPageShortcuts() {
|
|||
// the pages above have their own shortcuts sub-classes instantiated elsewhere
|
||||
// TODO: replace this whitelist with something more automated/maintainable
|
||||
if (page && !pagesWithCustomShortcuts.includes(page)) {
|
||||
return new Shortcuts();
|
||||
import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts')
|
||||
.then(({ default: Shortcuts }) => new Shortcuts())
|
||||
.catch(() => {});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
import { ApolloMutation } from 'vue-apollo';
|
||||
import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import allVersionsMixin from '../../mixins/all_versions';
|
||||
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
|
||||
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
|
||||
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
|
||||
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
|
||||
import DesignNote from './design_note.vue';
|
||||
import DesignReplyForm from './design_reply_form.vue';
|
||||
import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
|
||||
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
|
||||
import ToggleRepliesWidget from './toggle_replies_widget.vue';
|
||||
import { hasErrors } from '../../utils/cache_update';
|
||||
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -136,21 +137,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
addDiscussionComment(
|
||||
store,
|
||||
{
|
||||
data: { createNote },
|
||||
},
|
||||
) {
|
||||
updateStoreAfterAddDiscussionComment(
|
||||
store,
|
||||
createNote,
|
||||
getDesignQuery,
|
||||
this.designVariables,
|
||||
this.discussion.id,
|
||||
);
|
||||
},
|
||||
onDone() {
|
||||
onDone({ data: { createNote } }) {
|
||||
if (hasErrors(createNote)) {
|
||||
createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
|
||||
}
|
||||
this.discussionComment = '';
|
||||
this.hideForm();
|
||||
if (this.shouldChangeResolvedStatus) {
|
||||
|
@ -278,7 +268,6 @@ export default {
|
|||
:variables="{
|
||||
input: mutationPayload,
|
||||
}"
|
||||
:update="addDiscussionComment"
|
||||
@done="onDone"
|
||||
@error="onCreateNoteError"
|
||||
>
|
||||
|
|
|
@ -7,15 +7,11 @@ import { extractCurrentDiscussion, extractDesign, extractDesigns } from './desig
|
|||
import {
|
||||
ADD_IMAGE_DIFF_NOTE_ERROR,
|
||||
UPDATE_IMAGE_DIFF_NOTE_ERROR,
|
||||
ADD_DISCUSSION_COMMENT_ERROR,
|
||||
designDeletionError,
|
||||
} from './error_messages';
|
||||
|
||||
const designsOf = data => data.project.issue.designCollection.designs;
|
||||
|
||||
const isParticipating = (design, username) =>
|
||||
design.issue.participants.nodes.some(participant => participant.username === username);
|
||||
|
||||
const deleteDesignsFromStore = (store, query, selectedDesigns) => {
|
||||
const sourceData = store.readQuery(query);
|
||||
|
||||
|
@ -57,36 +53,6 @@ const addNewVersionToStore = (store, query, version) => {
|
|||
});
|
||||
};
|
||||
|
||||
const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
|
||||
const sourceData = store.readQuery({
|
||||
query,
|
||||
variables: queryVariables,
|
||||
});
|
||||
|
||||
const newParticipant = {
|
||||
__typename: 'User',
|
||||
...createNote.note.author,
|
||||
};
|
||||
|
||||
const data = produce(sourceData, draftData => {
|
||||
const design = extractDesign(draftData);
|
||||
const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
|
||||
currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
|
||||
|
||||
if (!isParticipating(design, createNote.note.author.username)) {
|
||||
design.issue.participants.nodes = [...design.issue.participants.nodes, newParticipant];
|
||||
}
|
||||
|
||||
design.notesCount += 1;
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query,
|
||||
variables: queryVariables,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
|
||||
const sourceData = store.readQuery({
|
||||
query,
|
||||
|
@ -246,20 +212,6 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const updateStoreAfterAddDiscussionComment = (
|
||||
store,
|
||||
data,
|
||||
query,
|
||||
queryVariables,
|
||||
discussionId,
|
||||
) => {
|
||||
if (hasErrors(data)) {
|
||||
onError(data, ADD_DISCUSSION_COMMENT_ERROR);
|
||||
} else {
|
||||
addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
|
||||
if (hasErrors(data)) {
|
||||
onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
|
||||
|
|
|
@ -40,10 +40,10 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getTrace();
|
||||
this.getLogs();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']),
|
||||
...mapActions('pipelines', ['fetchJobLogs', 'setDetailJob']),
|
||||
scrollDown() {
|
||||
if (this.$refs.buildTrace) {
|
||||
this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight);
|
||||
|
@ -66,8 +66,8 @@ export default {
|
|||
this.scrollPos = '';
|
||||
}
|
||||
}),
|
||||
getTrace() {
|
||||
return this.fetchJobTrace().then(() => this.scrollDown());
|
||||
getLogs() {
|
||||
return this.fetchJobLogs().then(() => this.scrollDown());
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -118,31 +118,31 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
|
||||
export const receiveJobTraceError = ({ commit, dispatch }) => {
|
||||
export const requestJobLogs = ({ commit }) => commit(types.REQUEST_JOB_LOGS);
|
||||
export const receiveJobLogsError = ({ commit, dispatch }) => {
|
||||
dispatch(
|
||||
'setErrorMessage',
|
||||
{
|
||||
text: __('An error occurred while fetching the job trace.'),
|
||||
text: __('An error occurred while fetching the job logs.'),
|
||||
action: () =>
|
||||
dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
|
||||
dispatch('fetchJobLogs').then(() => dispatch('setErrorMessage', null, { root: true })),
|
||||
actionText: __('Please try again'),
|
||||
actionPayload: null,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
commit(types.RECEIVE_JOB_TRACE_ERROR);
|
||||
commit(types.RECEIVE_JOB_LOGS_ERROR);
|
||||
};
|
||||
export const receiveJobTraceSuccess = ({ commit }, data) =>
|
||||
commit(types.RECEIVE_JOB_TRACE_SUCCESS, data);
|
||||
export const receiveJobLogsSuccess = ({ commit }, data) =>
|
||||
commit(types.RECEIVE_JOB_LOGS_SUCCESS, data);
|
||||
|
||||
export const fetchJobTrace = ({ dispatch, state }) => {
|
||||
dispatch('requestJobTrace');
|
||||
export const fetchJobLogs = ({ dispatch, state }) => {
|
||||
dispatch('requestJobLogs');
|
||||
|
||||
return axios
|
||||
.get(`${state.detailJob.path}/trace`, { params: { format: 'json' } })
|
||||
.then(({ data }) => dispatch('receiveJobTraceSuccess', data))
|
||||
.catch(() => dispatch('receiveJobTraceError'));
|
||||
.then(({ data }) => dispatch('receiveJobLogsSuccess', data))
|
||||
.catch(() => dispatch('receiveJobLogsError'));
|
||||
};
|
||||
|
||||
export const resetLatestPipeline = ({ commit }) => {
|
||||
|
|
|
@ -10,6 +10,6 @@ export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
|
|||
|
||||
export const SET_DETAIL_JOB = 'SET_DETAIL_JOB';
|
||||
|
||||
export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE';
|
||||
export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR';
|
||||
export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS';
|
||||
export const REQUEST_JOB_LOGS = 'REQUEST_JOB_LOGS';
|
||||
export const RECEIVE_JOB_LOGS_ERROR = 'RECEIVE_JOB_LOGS_ERROR';
|
||||
export const RECEIVE_JOB_LOGS_SUCCESS = 'RECEIVE_JOB_LOGS_SUCCESS';
|
||||
|
|
|
@ -66,13 +66,13 @@ export default {
|
|||
[types.SET_DETAIL_JOB](state, job) {
|
||||
state.detailJob = { ...job };
|
||||
},
|
||||
[types.REQUEST_JOB_TRACE](state) {
|
||||
[types.REQUEST_JOB_LOGS](state) {
|
||||
state.detailJob.isLoading = true;
|
||||
},
|
||||
[types.RECEIVE_JOB_TRACE_ERROR](state) {
|
||||
[types.RECEIVE_JOB_LOGS_ERROR](state) {
|
||||
state.detailJob.isLoading = false;
|
||||
},
|
||||
[types.RECEIVE_JOB_TRACE_SUCCESS](state, data) {
|
||||
[types.RECEIVE_JOB_LOGS_SUCCESS](state, data) {
|
||||
state.detailJob.isLoading = false;
|
||||
state.detailJob.output = data.html;
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module MergeRequests
|
||||
class RefreshService < MergeRequests::BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
attr_reader :push
|
||||
|
||||
def execute(oldrev, newrev, ref)
|
||||
|
@ -23,25 +24,37 @@ module MergeRequests
|
|||
post_merge_manually_merged
|
||||
link_forks_lfs_objects
|
||||
reload_merge_requests
|
||||
outdate_suggestions
|
||||
refresh_pipelines_on_merge_requests
|
||||
abort_auto_merges
|
||||
abort_ff_merge_requests_with_when_pipeline_succeeds
|
||||
mark_pending_todos_done
|
||||
cache_merge_requests_closing_issues
|
||||
|
||||
# Leave a system note if a branch was deleted/added
|
||||
if @push.branch_added? || @push.branch_removed?
|
||||
comment_mr_branch_presence_changed
|
||||
merge_requests_for_source_branch.each do |mr|
|
||||
outdate_suggestions(mr)
|
||||
refresh_pipelines_on_merge_requests(mr)
|
||||
abort_auto_merges(mr)
|
||||
mark_pending_todos_done(mr)
|
||||
end
|
||||
|
||||
notify_about_push
|
||||
mark_mr_as_wip_from_commits
|
||||
execute_mr_web_hooks
|
||||
abort_ff_merge_requests_with_when_pipeline_succeeds
|
||||
cache_merge_requests_closing_issues
|
||||
|
||||
merge_requests_for_source_branch.each do |mr|
|
||||
# Leave a system note if a branch was deleted/added
|
||||
if branch_added_or_removed?
|
||||
comment_mr_branch_presence_changed(mr)
|
||||
end
|
||||
|
||||
notify_about_push(mr)
|
||||
mark_mr_as_wip_from_commits(mr)
|
||||
execute_mr_web_hooks(mr)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def branch_added_or_removed?
|
||||
strong_memoize(:branch_added_or_removed) do
|
||||
@push.branch_added? || @push.branch_removed?
|
||||
end
|
||||
end
|
||||
|
||||
def close_upon_missing_source_branch_ref
|
||||
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
|
||||
# create an `empty` diff for `closed` MRs without a source branch, keeping
|
||||
|
@ -140,25 +153,22 @@ module MergeRequests
|
|||
merge_request.source_branch == @push.branch_name
|
||||
end
|
||||
|
||||
def outdate_suggestions
|
||||
outdate_service = Suggestions::OutdateService.new
|
||||
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
outdate_service.execute(merge_request)
|
||||
end
|
||||
def outdate_suggestions(merge_request)
|
||||
outdate_service.execute(merge_request)
|
||||
end
|
||||
|
||||
def refresh_pipelines_on_merge_requests
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
create_pipeline_for(merge_request, current_user)
|
||||
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
|
||||
end
|
||||
def outdate_service
|
||||
@outdate_service ||= Suggestions::OutdateService.new
|
||||
end
|
||||
|
||||
def abort_auto_merges
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
abort_auto_merge(merge_request, 'source branch was updated')
|
||||
end
|
||||
def refresh_pipelines_on_merge_requests(merge_request)
|
||||
create_pipeline_for(merge_request, current_user)
|
||||
|
||||
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
|
||||
end
|
||||
|
||||
def abort_auto_merges(merge_request)
|
||||
abort_auto_merge(merge_request, 'source branch was updated')
|
||||
end
|
||||
|
||||
def abort_ff_merge_requests_with_when_pipeline_succeeds
|
||||
|
@ -187,10 +197,8 @@ module MergeRequests
|
|||
.with_auto_merge_enabled
|
||||
end
|
||||
|
||||
def mark_pending_todos_done
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
todo_service.merge_request_push(merge_request, @current_user)
|
||||
end
|
||||
def mark_pending_todos_done(merge_request)
|
||||
todo_service.merge_request_push(merge_request, @current_user)
|
||||
end
|
||||
|
||||
def find_new_commits
|
||||
|
@ -218,62 +226,54 @@ module MergeRequests
|
|||
end
|
||||
|
||||
# Add comment about branches being deleted or added to merge requests
|
||||
def comment_mr_branch_presence_changed
|
||||
def comment_mr_branch_presence_changed(merge_request)
|
||||
presence = @push.branch_added? ? :add : :delete
|
||||
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
SystemNoteService.change_branch_presence(
|
||||
merge_request, merge_request.project, @current_user,
|
||||
:source, @push.branch_name, presence)
|
||||
end
|
||||
SystemNoteService.change_branch_presence(
|
||||
merge_request, merge_request.project, @current_user,
|
||||
:source, @push.branch_name, presence)
|
||||
end
|
||||
|
||||
# Add comment about pushing new commits to merge requests and send nofitication emails
|
||||
def notify_about_push
|
||||
def notify_about_push(merge_request)
|
||||
return unless @commits.present?
|
||||
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
mr_commit_ids = Set.new(merge_request.commit_shas)
|
||||
mr_commit_ids = Set.new(merge_request.commit_shas)
|
||||
|
||||
new_commits, existing_commits = @commits.partition do |commit|
|
||||
mr_commit_ids.include?(commit.id)
|
||||
end
|
||||
|
||||
SystemNoteService.add_commits(merge_request, merge_request.project,
|
||||
@current_user, new_commits,
|
||||
existing_commits, @push.oldrev)
|
||||
|
||||
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
|
||||
new_commits, existing_commits = @commits.partition do |commit|
|
||||
mr_commit_ids.include?(commit.id)
|
||||
end
|
||||
|
||||
SystemNoteService.add_commits(merge_request, merge_request.project,
|
||||
@current_user, new_commits,
|
||||
existing_commits, @push.oldrev)
|
||||
|
||||
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
|
||||
end
|
||||
|
||||
def mark_mr_as_wip_from_commits
|
||||
def mark_mr_as_wip_from_commits(merge_request)
|
||||
return unless @commits.present?
|
||||
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
commit_shas = merge_request.commit_shas
|
||||
commit_shas = merge_request.commit_shas
|
||||
|
||||
wip_commit = @commits.detect do |commit|
|
||||
commit.work_in_progress? && commit_shas.include?(commit.sha)
|
||||
end
|
||||
wip_commit = @commits.detect do |commit|
|
||||
commit.work_in_progress? && commit_shas.include?(commit.sha)
|
||||
end
|
||||
|
||||
if wip_commit && !merge_request.work_in_progress?
|
||||
merge_request.update(title: merge_request.wip_title)
|
||||
SystemNoteService.add_merge_request_wip_from_commit(
|
||||
merge_request,
|
||||
merge_request.project,
|
||||
@current_user,
|
||||
wip_commit
|
||||
)
|
||||
end
|
||||
if wip_commit && !merge_request.work_in_progress?
|
||||
merge_request.update(title: merge_request.wip_title)
|
||||
SystemNoteService.add_merge_request_wip_from_commit(
|
||||
merge_request,
|
||||
merge_request.project,
|
||||
@current_user,
|
||||
wip_commit
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Call merge request webhook with update branches
|
||||
def execute_mr_web_hooks
|
||||
merge_requests_for_source_branch.each do |merge_request|
|
||||
execute_hooks(merge_request, 'update', old_rev: @push.oldrev)
|
||||
end
|
||||
def execute_mr_web_hooks(merge_request)
|
||||
execute_hooks(merge_request, 'update', old_rev: @push.oldrev)
|
||||
end
|
||||
|
||||
# If the merge requests closes any issues, save this information in the
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rename job trace to job logs in IDE code
|
||||
merge_request: 41522
|
||||
author: Kev @KevSlashNull
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Check if usage ping enabled for all tracking using Redis HLL
|
||||
merge_request: 41562
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve design discussion bug where a comment is added twice
|
||||
merge_request: 41687
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/hide-latest-versions-from-ui.yml
Normal file
5
changelogs/unreleased/hide-latest-versions-from-ui.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Hide the latest version of templates from the template selector
|
||||
merge_request: 40937
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/refactor-deployment-templates.yml
Normal file
5
changelogs/unreleased/refactor-deployment-templates.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Jobs/Deploy/ECS.gitlab-ci.yml to the top level of AutoDevOps template
|
||||
merge_request: 41096
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/refresh-service-optimisations.yml
Normal file
5
changelogs/unreleased/refresh-service-optimisations.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reduce MergeRequest::RefreshService loops
|
||||
merge_request: 40135
|
||||
author:
|
||||
type: performance
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
# Suggestion: gitlab.ContractionsDiscard
|
||||
#
|
||||
# Suggests a list of agreed-upon contractions to discard.
|
||||
#
|
||||
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
|
||||
extends: substitution
|
||||
message: 'Use "%s" instead of "%s", for a friendly, informal tone.'
|
||||
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#language
|
||||
level: suggestion
|
||||
nonword: false
|
||||
ignorecase: true
|
||||
swap:
|
||||
|
||||
# Uncommon contractions are not ok
|
||||
aren't: are not
|
||||
couldn't: could not
|
||||
didn't: did not
|
||||
doesn't: does not
|
||||
hasn't: has not
|
||||
how's: how is
|
||||
isn't: is not
|
||||
shouldn't: should not
|
||||
they're: they are
|
||||
wasn't: was not
|
||||
weren't: were not
|
||||
we've: we have
|
||||
what's: what is
|
||||
when's: when is
|
||||
where's: where is
|
||||
who's: who is
|
||||
why's: why is
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
# Suggestion: gitlab.ContractionsKeep
|
||||
#
|
||||
# Suggests a list of agreed-upon contractions to keep.
|
||||
#
|
||||
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
|
||||
extends: substitution
|
||||
message: 'Use "%s" instead of "%s", for a friendly, informal tone.'
|
||||
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#language
|
||||
level: suggestion
|
||||
nonword: false
|
||||
ignorecase: true
|
||||
swap:
|
||||
|
||||
# Common contractions are ok
|
||||
it is: it's
|
||||
can not: can't
|
||||
cannot: can't
|
||||
do not: don't
|
||||
have not: haven't
|
||||
that is: that's
|
||||
we are: we're
|
||||
would not: wouldn't
|
||||
you are: you're
|
||||
you have: you've
|
|
@ -58,7 +58,6 @@ plays an important role in your deployment, we suggest you benchmark to find the
|
|||
optimal configuration:
|
||||
|
||||
- The safest option is to start with single-threaded Puma. When working with
|
||||
Rugged, single-threaded Puma does work the same as Unicorn.
|
||||
|
||||
- To force Rugged auto detect with multi-threaded Puma, you can use [feature
|
||||
flags](../../development/gitaly.md#legacy-rugged-code).
|
||||
Rugged, single-threaded Puma works the same as Unicorn.
|
||||
- To force Rugged to be used with multi-threaded Puma, you can use
|
||||
[feature flags](../../development/gitaly.md#legacy-rugged-code).
|
||||
|
|
|
@ -15,15 +15,15 @@ Get a list of deployments in a project.
|
|||
GET /projects/:id/deployments
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `order_by`| string | no | Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref` fields. Default is `id` |
|
||||
| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc` |
|
||||
| `updated_after` | datetime | no | Return deployments updated after the specified date |
|
||||
| `updated_before` | datetime | no | Return deployments updated before the specified date |
|
||||
| `environment` | string | no | The name of the environment to filter deployments by |
|
||||
| `status` | string | no | The status to filter deployments by |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `order_by` | string | no | Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref` fields. Default is `id` |
|
||||
| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc` |
|
||||
| `updated_after` | datetime | no | Return deployments updated after the specified date |
|
||||
| `updated_before` | datetime | no | Return deployments updated before the specified date |
|
||||
| `environment` | string | no | The [name of the environment](../ci/environments/index.md#defining-environments) to filter deployments by |
|
||||
| `status` | string | no | The status to filter deployments by |
|
||||
|
||||
The status attribute can be one of the following values:
|
||||
|
||||
|
@ -278,14 +278,14 @@ Example of response
|
|||
POST /projects/:id/deployments
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|---------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment` | string | yes | The name of the environment to create the deployment for |
|
||||
| `sha` | string | yes | The SHA of the commit that is deployed |
|
||||
| `ref` | string | yes | The name of the branch or tag that is deployed |
|
||||
| `tag` | boolean | yes | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
|
||||
| `status` | string | yes | The status of the deployment |
|
||||
| Attribute | Type | Required | Description |
|
||||
|---------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment` | string | yes | The [name of the environment](../ci/environments/index.md#defining-environments) to create the deployment for |
|
||||
| `sha` | string | yes | The SHA of the commit that is deployed |
|
||||
| `ref` | string | yes | The name of the branch or tag that is deployed |
|
||||
| `tag` | boolean | yes | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
|
||||
| `status` | string | yes | The status of the deployment |
|
||||
|
||||
The status can be one of the following values:
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ All template files reside in the `lib/gitlab/ci/templates` directory, and are ca
|
|||
| Sub-directory | Content | [Selectable in UI](#make-sure-the-new-template-can-be-selected-in-ui) |
|
||||
|----------------|--------------------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| `/AWS/*` | Cloud Deployment (AWS) related jobs | No |
|
||||
| `/Jobs/*` | Auto DevOps related jobs | Yes |
|
||||
| `/Jobs/*` | Auto DevOps related jobs | No |
|
||||
| `/Pages/*` | Static site generators for GitLab Pages (for example Jekyll) | Yes |
|
||||
| `/Security/*` | Security related jobs | Yes |
|
||||
| `/Verify/*` | Verify/testing related jobs | Yes |
|
||||
|
|
|
@ -540,35 +540,10 @@ tenses, words, and phrases:
|
|||
|
||||
### Contractions
|
||||
|
||||
- Use common contractions when it helps create a friendly and informal tone,
|
||||
especially in tutorials, instructional documentation, and
|
||||
[user interfaces](https://design.gitlab.com/content/punctuation/#contractions).
|
||||
(Tested in [`Contractions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/Contractions.yml).)
|
||||
Contractions can create a friendly and informal tone, especially in tutorials, instructional
|
||||
documentation, and [user interfaces](https://design.gitlab.com/content/punctuation/#contractions).
|
||||
|
||||
<!-- vale gitlab.ContractionsKeep = NO -->
|
||||
<!-- vale gitlab.ContractionsDiscard = NO -->
|
||||
<!-- vale gitlab.FutureTense = NO -->
|
||||
| Do | Don't |
|
||||
|----------|-----------|
|
||||
| it's | it is |
|
||||
| can't | cannot |
|
||||
| wouldn't | would not |
|
||||
| you're | you are |
|
||||
| you've | you have |
|
||||
| haven't | have not |
|
||||
| don't | do not |
|
||||
| we're | we are |
|
||||
| that's | that is |
|
||||
| won't | will not |
|
||||
|
||||
- Avoid less common contractions:
|
||||
|
||||
| Do | Don't |
|
||||
|--------------|-------------|
|
||||
| he would | he'd |
|
||||
| it will | it'll |
|
||||
| should have | should've |
|
||||
| there would | there'd |
|
||||
Some contractions should be avoided:
|
||||
|
||||
- Do not use contractions with a proper noun and a verb. For example:
|
||||
|
||||
|
@ -580,13 +555,13 @@ tenses, words, and phrases:
|
|||
|
||||
| Do | Don't |
|
||||
|-----------------------------|----------------------------|
|
||||
| Do *not* install X with Y | *Don't* install X with Y |
|
||||
| Do **not** install X with Y | **Don't** install X with Y |
|
||||
|
||||
- Do not use contractions in reference documentation. For example:
|
||||
|
||||
| Do | Don't |
|
||||
|------------------------------------------|----------------------------------------|
|
||||
| Do *not* set a limit greater than 1000 | *Don't* set a limit greater than 1000 |
|
||||
| Do **not** set a limit greater than 1000 | **Don't** set a limit greater than 1000 |
|
||||
| For `parameter1`, the default is 10 | For `parameter1`, the default's 10 |
|
||||
|
||||
- Avoid contractions in error messages. Examples:
|
||||
|
@ -596,10 +571,6 @@ tenses, words, and phrases:
|
|||
| Requests to localhost are not allowed | Requests to localhost aren't allowed |
|
||||
| Specified URL cannot be used | Specified URL can't be used |
|
||||
|
||||
<!-- vale gitlab.ContractionsKeep = YES -->
|
||||
<!-- vale gitlab.ContractionsDiscard = YES -->
|
||||
<!-- vale gitlab.FutureTense = YES -->
|
||||
|
||||
## Text
|
||||
|
||||
- [Write in Markdown](#markdown).
|
||||
|
|
|
@ -602,6 +602,174 @@ it('calls mutation on submitting form ', () => {
|
|||
});
|
||||
```
|
||||
|
||||
### Testing with mocked Apollo Client
|
||||
|
||||
To test the logic of Apollo cache updates, we might want to mock an Apollo Client in our unit tests. To separate tests with mocked client from 'usual' unit tests, it's recommended to create an additional component factory. This way we only create Apollo Client instance when it's necessary:
|
||||
|
||||
```javascript
|
||||
function createComponent() {...}
|
||||
|
||||
function createComponentWithApollo() {...}
|
||||
```
|
||||
|
||||
We use [`mock-apollo-client`](https://www.npmjs.com/package/mock-apollo-client) library to mock Apollo client in tests.
|
||||
|
||||
```javascript
|
||||
import { createMockClient } from 'mock-apollo-client';
|
||||
```
|
||||
|
||||
Then we need to inject `VueApollo` to Vue local instance (`localVue.use()` can also be called within `createComponentWithApollo()`)
|
||||
|
||||
```javascript
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
```
|
||||
|
||||
After this, on the global `describe`, we should create a variable for `fakeApollo`:
|
||||
|
||||
```javascript
|
||||
describe('Some component with Apollo mock', () => {
|
||||
let wrapper;
|
||||
let fakeApollo
|
||||
})
|
||||
```
|
||||
|
||||
Within component factory, we need to define an array of _handlers_ for every query or mutation:
|
||||
|
||||
```javascript
|
||||
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
||||
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
|
||||
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
|
||||
|
||||
describe('Some component with Apollo mock', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
function createComponentWithApollo() {
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
];
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
After this, we need to create a mock Apollo Client instance using a helper:
|
||||
|
||||
```javascript
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
|
||||
describe('Some component with Apollo mock', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
function createComponentWithApollo() {
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
When mocking resolved values, make sure the structure of the response is the same as actual API response: i.e. root property should be `data` for example
|
||||
|
||||
When testing queries, please keep in mind they are promises, so they need to be _resolved_ to render a result. Without resolving, we can check the `loading` state of the query:
|
||||
|
||||
```javascript
|
||||
it('renders a loading state', () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
expect(wrapper.find(LoadingSpinner).exists()).toBe(true)
|
||||
});
|
||||
|
||||
it('renders designs list', async () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDesigns()).toHaveLength(3);
|
||||
});
|
||||
```
|
||||
|
||||
If we need to test a query error, we need to mock a rejected value as request handler:
|
||||
|
||||
```javascript
|
||||
function createComponentWithApollo() {
|
||||
...
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockRejectedValue(new Error('GraphQL error')],
|
||||
];
|
||||
...
|
||||
}
|
||||
...
|
||||
|
||||
it('renders error if query fails', async () => {
|
||||
createComponent()
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('.test-error').exists()).toBe(true)
|
||||
})
|
||||
```
|
||||
|
||||
Request handlers can also be passed to component factory as a parameter.
|
||||
|
||||
Mutations could be tested the same way with a few additional `nextTick`s to get the updated result:
|
||||
|
||||
```javascript
|
||||
function createComponentWithApollo({
|
||||
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
||||
}) {
|
||||
moveDesignHandler = moveHandler;
|
||||
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
[moveDesignMutation, moveDesignHandler],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
}
|
||||
...
|
||||
it('calls a mutation with correct parameters and reorders designs', async () => {
|
||||
createComponentWithApollo({});
|
||||
|
||||
wrapper.find(VueDraggable).vm.$emit('change', {
|
||||
moved: {
|
||||
newIndex: 0,
|
||||
element: designToMove,
|
||||
},
|
||||
});
|
||||
|
||||
expect(moveDesignHandler).toHaveBeenCalled();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
findDesigns()
|
||||
.at(0)
|
||||
.props('id'),
|
||||
).toBe('2');
|
||||
});
|
||||
```
|
||||
|
||||
## Handling errors
|
||||
|
||||
GitLab's GraphQL mutations currently have two distinct error modes: [Top-level](#top-level-errors) and [errors-as-data](#errors-as-data).
|
||||
|
|
|
@ -35,7 +35,3 @@ the instance license.
|
|||
```ruby
|
||||
License.feature_available?(:feature_symbol)
|
||||
```
|
||||
|
||||
## Enabling promo features on GitLab.com
|
||||
|
||||
A paid feature can be made available to everyone on GitLab.com by enabling the feature flag `"promo_#{feature}"`.
|
||||
|
|
|
@ -106,6 +106,7 @@ as shown in the following table:
|
|||
| [Presentation of JSON Report in Merge Request](#overview) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [Interaction with Vulnerabilities](#interacting-with-the-vulnerabilities) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [Access to Security Dashboard](#security-dashboard) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [Configure SAST in the UI](#configure-sast-in-the-ui) | **{dotted-circle}** | **{check-circle}** |
|
||||
|
||||
## Contribute your scanner
|
||||
|
||||
|
@ -142,7 +143,7 @@ The results are saved as a
|
|||
that you can later download and analyze. Due to implementation limitations, we
|
||||
always take the latest SAST artifact available.
|
||||
|
||||
### Configure SAST in the UI
|
||||
### Configure SAST in the UI **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3659) in GitLab Ultimate 13.3.
|
||||
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/232862) in GitLab Ultimate 13.4.
|
||||
|
|
|
@ -9,7 +9,6 @@ type: reference
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6916)
|
||||
in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
|
||||
> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) added in GitLab Starter 12.1.
|
||||
> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
|
||||
|
||||
## Introduction
|
||||
|
@ -108,41 +107,54 @@ in the `.gitignore` file followed by one or more of:
|
|||
- A user's `@username`.
|
||||
- A user's email address.
|
||||
- The `@name` of one or more groups that should be owners of the file.
|
||||
- Lines starting with `#` are escaped.
|
||||
|
||||
Groups must be added as [members of the project](members/index.md),
|
||||
or they will be ignored.
|
||||
The order in which the paths are defined is significant: the last pattern that
|
||||
matches a given path will be used to find the code owners.
|
||||
|
||||
Starting in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/32432),
|
||||
you can additionally specify groups or subgroups from the project's upper group
|
||||
hierarchy as potential code owners, without having to invite them specifically
|
||||
to the project. Groups outside the project's hierarchy or children beneath the
|
||||
hierarchy must still be explicitly invited to the project in order to show as
|
||||
Code Owners.
|
||||
### Groups as Code Owners
|
||||
|
||||
For example, consider the following hierarchy for the example project
|
||||
`example_project`:
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) in GitLab Starter 12.1.
|
||||
> - Group and subgroup hierarchy support was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32432) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.0.
|
||||
|
||||
Groups and subgroups members are inherited as eligible Code Owners to a
|
||||
project, as long as the hierarchy is respected.
|
||||
|
||||
For example, consider a given group called "Group X" (slug `group-x`) and a
|
||||
"Subgroup Y" (slug `group-x/subgroup-y`) that belongs to the Group X, and
|
||||
suppose you have a project called "Project A" within the group and a
|
||||
"Project B" within the subgroup.
|
||||
|
||||
The eligible Code Owners to Project B are both the members of the Group X and
|
||||
the Subgroup Y. And the eligible Code Owners to the Project A are just the
|
||||
members of the Group X, given that Project A doesn't belong to the Subgroup Y:
|
||||
|
||||
![Eligible Code Owners](img/code_owners_members_v13_4.png)
|
||||
|
||||
But you have the option to [invite](members/share_project_with_groups.md)
|
||||
the Subgroup Y to the Project A so that their members also become eligible
|
||||
Code Owners:
|
||||
|
||||
![Invite subgroup members to become eligible Code Owners](img/code_owners_invite_members_v13_4.png)
|
||||
|
||||
Once invited, any member (`@user`) of the group or subgroup can be set
|
||||
as Code Owner to files of the Project A or B, as well as the entire Group X
|
||||
(`@group-x`) or Subgroup Y (`@group-x/subgroup-y`), as exemplified below:
|
||||
|
||||
```plaintext
|
||||
group >> sub-group >> sub-subgroup >> example_project >> file.md
|
||||
# A member of the group or subgroup as Code Owner to a file
|
||||
file.md @user
|
||||
|
||||
# All group members as Code Owners to a file
|
||||
file.md @group-x
|
||||
|
||||
# All subgroup members as Code Owners to a file
|
||||
file.md @group-x/subgroup-y
|
||||
|
||||
# All group and subgroup members as Code Owners to a file
|
||||
file.md @group-x @group-x/subgroup-y
|
||||
```
|
||||
|
||||
Any of the following groups would be eligible to be specified as code owners:
|
||||
|
||||
- `@group`
|
||||
- `@group/sub-group`
|
||||
- `@group/sub-group/sub-subgroup`
|
||||
|
||||
In addition, any groups that have been invited to the project using the
|
||||
**Members** tool will also be recognized as eligible code owners.
|
||||
|
||||
The order in which the paths are defined is significant: the last
|
||||
pattern that matches a given path will be used to find the code
|
||||
owners.
|
||||
|
||||
Starting a line with a `#` indicates a comment. This needs to be
|
||||
escaped using `\#` to address files for which the name starts with a
|
||||
`#`.
|
||||
|
||||
### Code Owners Sections **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12137) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
|
BIN
doc/user/project/img/code_owners_invite_members_v13_4.png
Normal file
BIN
doc/user/project/img/code_owners_invite_members_v13_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
doc/user/project/img/code_owners_members_v13_4.png
Normal file
BIN
doc/user/project/img/code_owners_members_v13_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -84,14 +84,17 @@ module API
|
|||
end
|
||||
params do
|
||||
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
|
||||
optional :title, type: String, allow_blank: false, desc: 'The title of the snippet'
|
||||
optional :file_name, type: String, desc: 'The file name of the snippet'
|
||||
optional :content, type: String, allow_blank: false, desc: 'The content of the snippet'
|
||||
optional :description, type: String, desc: 'The description of a snippet'
|
||||
optional :file_name, type: String, desc: 'The file name of the snippet'
|
||||
optional :title, type: String, allow_blank: false, desc: 'The title of the snippet'
|
||||
optional :visibility, type: String,
|
||||
values: Gitlab::VisibilityLevel.string_values,
|
||||
desc: 'The visibility of the snippet'
|
||||
at_least_one_of :title, :file_name, :content, :visibility
|
||||
|
||||
use :update_file_params
|
||||
|
||||
at_least_one_of :title, :file_name, :content, :files, :visibility
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
put ":id/snippets/:snippet_id" do
|
||||
|
@ -100,8 +103,9 @@ module API
|
|||
|
||||
authorize! :update_snippet, snippet
|
||||
|
||||
snippet_params = declared_params(include_missing: false)
|
||||
.merge(request: request, api: true)
|
||||
validate_params_for_multiple_files(snippet)
|
||||
|
||||
snippet_params = process_update_params(declared_params(include_missing: false))
|
||||
|
||||
service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet)
|
||||
snippet = service_response.payload[:snippet]
|
||||
|
|
|
@ -150,16 +150,22 @@ workflow:
|
|||
- exists:
|
||||
- .static
|
||||
|
||||
# NOTE: These links point to the latest templates for development in GitLab canonical project,
|
||||
# therefore the actual templates that were included for Auto DevOps pipelines
|
||||
# could be different from the contents in the links.
|
||||
# To view the actual templates, please replace `master` to the specific GitLab version when
|
||||
# the Auto DevOps pipeline started running e.g. `v13.0.2-ee`.
|
||||
include:
|
||||
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
|
||||
- template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
|
||||
- template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
|
||||
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
|
||||
- template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
|
||||
- template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
|
||||
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
|
||||
- template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
|
||||
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
|
||||
- template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
|
||||
- template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
|
||||
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
|
||||
- template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
|
||||
- template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
|
||||
- template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
|
||||
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
|
||||
- template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3"
|
||||
dependencies: []
|
||||
|
||||
include:
|
||||
- template: Jobs/Deploy/ECS.gitlab-ci.yml
|
||||
|
||||
review:
|
||||
extends: .auto-deploy
|
||||
stage: review
|
||||
|
|
|
@ -5,10 +5,10 @@ module Gitlab
|
|||
module Template
|
||||
module Finders
|
||||
class GlobalTemplateFinder < BaseTemplateFinder
|
||||
def initialize(base_dir, extension, categories = {}, exclusions: [])
|
||||
def initialize(base_dir, extension, categories = {}, excluded_patterns: [])
|
||||
@categories = categories
|
||||
@extension = extension
|
||||
@exclusions = exclusions
|
||||
@excluded_patterns = excluded_patterns
|
||||
|
||||
super(base_dir)
|
||||
end
|
||||
|
@ -43,7 +43,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def excluded?(file_name)
|
||||
@exclusions.include?(file_name)
|
||||
@excluded_patterns.any? { |pattern| pattern.match?(file_name) }
|
||||
end
|
||||
|
||||
def select_directory(file_name)
|
||||
|
|
|
@ -3,12 +3,16 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class GitlabCiYmlTemplate < BaseTemplate
|
||||
BASE_EXCLUDED_PATTERNS = [%r{\.latest$}].freeze
|
||||
|
||||
def content
|
||||
explanation = "# This file is a template, and might need editing before it works on your project."
|
||||
[explanation, super].join("\n")
|
||||
end
|
||||
|
||||
class << self
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def extension
|
||||
'.gitlab-ci.yml'
|
||||
end
|
||||
|
@ -22,10 +26,14 @@ module Gitlab
|
|||
}
|
||||
end
|
||||
|
||||
def disabled_templates
|
||||
%w[
|
||||
Verify/Browser-Performance
|
||||
]
|
||||
def excluded_patterns
|
||||
strong_memoize(:excluded_patterns) do
|
||||
BASE_EXCLUDED_PATTERNS + additional_excluded_patterns
|
||||
end
|
||||
end
|
||||
|
||||
def additional_excluded_patterns
|
||||
[%r{Verify/Browser-Performance}]
|
||||
end
|
||||
|
||||
def base_dir
|
||||
|
@ -34,7 +42,7 @@ module Gitlab
|
|||
|
||||
def finder(project = nil)
|
||||
Gitlab::Template::Finders::GlobalTemplateFinder.new(
|
||||
self.base_dir, self.extension, self.categories, exclusions: self.disabled_templates
|
||||
self.base_dir, self.extension, self.categories, excluded_patterns: self.excluded_patterns
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,6 +34,8 @@ module Gitlab
|
|||
# * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current)
|
||||
class << self
|
||||
def track_event(entity_id, event_name, time = Time.zone.now)
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled?
|
||||
|
||||
event = event_for(event_name)
|
||||
|
||||
raise UnknownEvent.new("Unknown event #{event_name}") unless event.present?
|
||||
|
|
|
@ -2708,7 +2708,7 @@ msgstr ""
|
|||
msgid "An error occurred while fetching the job log."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching the job trace."
|
||||
msgid "An error occurred while fetching the job logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching the job."
|
||||
|
|
|
@ -32,7 +32,6 @@ describe('Design discussions component', () => {
|
|||
|
||||
const mutationVariables = {
|
||||
mutation: createNoteMutation,
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
input: {
|
||||
noteableId: 'noteable-id',
|
||||
|
@ -41,7 +40,7 @@ describe('Design discussions component', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const mutate = jest.fn(() => Promise.resolve());
|
||||
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
|
||||
const $apollo = {
|
||||
mutate,
|
||||
};
|
||||
|
@ -227,7 +226,7 @@ describe('Design discussions component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls mutation on submitting form and closes the form', () => {
|
||||
it('calls mutation on submitting form and closes the form', async () => {
|
||||
createComponent(
|
||||
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||
{ discussionComment: 'test', isFormRendered: true },
|
||||
|
@ -236,13 +235,10 @@ describe('Design discussions component', () => {
|
|||
findReplyForm().vm.$emit('submitForm');
|
||||
expect(mutate).toHaveBeenCalledWith(mutationVariables);
|
||||
|
||||
return mutate()
|
||||
.then(() => {
|
||||
return wrapper.vm.$nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(findReplyForm().exists()).toBe(false);
|
||||
});
|
||||
await mutate();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findReplyForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('clears the discussion comment on closing comment form', () => {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import {
|
||||
updateStoreAfterDesignsDelete,
|
||||
updateStoreAfterAddDiscussionComment,
|
||||
updateStoreAfterAddImageDiffNote,
|
||||
updateStoreAfterUploadDesign,
|
||||
updateStoreAfterUpdateImageDiffNote,
|
||||
} from '~/design_management/utils/cache_update';
|
||||
import {
|
||||
designDeletionError,
|
||||
ADD_DISCUSSION_COMMENT_ERROR,
|
||||
ADD_IMAGE_DIFF_NOTE_ERROR,
|
||||
UPDATE_IMAGE_DIFF_NOTE_ERROR,
|
||||
} from '~/design_management/utils/error_messages';
|
||||
|
@ -28,12 +26,11 @@ describe('Design Management cache update', () => {
|
|||
|
||||
describe('error handling', () => {
|
||||
it.each`
|
||||
fnName | subject | errorMessage | extraArgs
|
||||
${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
|
||||
${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]}
|
||||
${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
|
||||
${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
fnName | subject | errorMessage | extraArgs
|
||||
${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
|
||||
${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
|
||||
${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
`('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
|
||||
expect(createFlash).not.toHaveBeenCalled();
|
||||
expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('IDE jobs detail view', () => {
|
|||
beforeEach(() => {
|
||||
vm = createComponent();
|
||||
|
||||
jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
|
||||
jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -36,8 +36,8 @@ describe('IDE jobs detail view', () => {
|
|||
vm = vm.$mount();
|
||||
});
|
||||
|
||||
it('calls fetchJobTrace', () => {
|
||||
expect(vm.fetchJobTrace).toHaveBeenCalled();
|
||||
it('calls fetchJobLogs', () => {
|
||||
expect(vm.fetchJobLogs).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('scrolls to bottom', () => {
|
||||
|
@ -96,7 +96,7 @@ describe('IDE jobs detail view', () => {
|
|||
describe('scroll buttons', () => {
|
||||
beforeEach(() => {
|
||||
vm = createComponent();
|
||||
jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
|
||||
jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -15,10 +15,10 @@ import {
|
|||
fetchJobs,
|
||||
toggleStageCollapsed,
|
||||
setDetailJob,
|
||||
requestJobTrace,
|
||||
receiveJobTraceError,
|
||||
receiveJobTraceSuccess,
|
||||
fetchJobTrace,
|
||||
requestJobLogs,
|
||||
receiveJobLogsError,
|
||||
receiveJobLogsSuccess,
|
||||
fetchJobLogs,
|
||||
resetLatestPipeline,
|
||||
} from '~/ide/stores/modules/pipelines/actions';
|
||||
import state from '~/ide/stores/modules/pipelines/state';
|
||||
|
@ -324,24 +324,24 @@ describe('IDE pipelines actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('requestJobTrace', () => {
|
||||
describe('requestJobLogs', () => {
|
||||
it('commits request', done => {
|
||||
testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done);
|
||||
testAction(requestJobLogs, null, mockedState, [{ type: types.REQUEST_JOB_LOGS }], [], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveJobTraceError', () => {
|
||||
describe('receiveJobLogsError', () => {
|
||||
it('commits error', done => {
|
||||
testAction(
|
||||
receiveJobTraceError,
|
||||
receiveJobLogsError,
|
||||
null,
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_JOB_TRACE_ERROR }],
|
||||
[{ type: types.RECEIVE_JOB_LOGS_ERROR }],
|
||||
[
|
||||
{
|
||||
type: 'setErrorMessage',
|
||||
payload: {
|
||||
text: 'An error occurred while fetching the job trace.',
|
||||
text: 'An error occurred while fetching the job logs.',
|
||||
action: expect.any(Function),
|
||||
actionText: 'Please try again',
|
||||
actionPayload: null,
|
||||
|
@ -353,20 +353,20 @@ describe('IDE pipelines actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('receiveJobTraceSuccess', () => {
|
||||
describe('receiveJobLogsSuccess', () => {
|
||||
it('commits data', done => {
|
||||
testAction(
|
||||
receiveJobTraceSuccess,
|
||||
receiveJobLogsSuccess,
|
||||
'data',
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }],
|
||||
[{ type: types.RECEIVE_JOB_LOGS_SUCCESS, payload: 'data' }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchJobTrace', () => {
|
||||
describe('fetchJobLogs', () => {
|
||||
beforeEach(() => {
|
||||
mockedState.detailJob = { path: `${TEST_HOST}/project/builds` };
|
||||
});
|
||||
|
@ -379,20 +379,20 @@ describe('IDE pipelines actions', () => {
|
|||
|
||||
it('dispatches request', done => {
|
||||
testAction(
|
||||
fetchJobTrace,
|
||||
fetchJobLogs,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[
|
||||
{ type: 'requestJobTrace' },
|
||||
{ type: 'receiveJobTraceSuccess', payload: { html: 'html' } },
|
||||
{ type: 'requestJobLogs' },
|
||||
{ type: 'receiveJobLogsSuccess', payload: { html: 'html' } },
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends get request to correct URL', () => {
|
||||
fetchJobTrace({
|
||||
fetchJobLogs({
|
||||
state: mockedState,
|
||||
|
||||
dispatch() {},
|
||||
|
@ -410,11 +410,11 @@ describe('IDE pipelines actions', () => {
|
|||
|
||||
it('dispatches error', done => {
|
||||
testAction(
|
||||
fetchJobTrace,
|
||||
fetchJobLogs,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }],
|
||||
[{ type: 'requestJobLogs' }, { type: 'receiveJobLogsError' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -175,37 +175,37 @@ describe('IDE pipelines mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_JOB_TRACE', () => {
|
||||
describe('REQUEST_JOB_LOGS', () => {
|
||||
beforeEach(() => {
|
||||
mockedState.detailJob = { ...jobs[0] };
|
||||
});
|
||||
|
||||
it('sets loading on detail job', () => {
|
||||
mutations[types.REQUEST_JOB_TRACE](mockedState);
|
||||
mutations[types.REQUEST_JOB_LOGS](mockedState);
|
||||
|
||||
expect(mockedState.detailJob.isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_JOB_TRACE_ERROR', () => {
|
||||
describe('RECEIVE_JOB_LOGS_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
mockedState.detailJob = { ...jobs[0], isLoading: true };
|
||||
});
|
||||
|
||||
it('sets loading to false on detail job', () => {
|
||||
mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState);
|
||||
mutations[types.RECEIVE_JOB_LOGS_ERROR](mockedState);
|
||||
|
||||
expect(mockedState.detailJob.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_JOB_TRACE_SUCCESS', () => {
|
||||
describe('RECEIVE_JOB_LOGS_SUCCESS', () => {
|
||||
beforeEach(() => {
|
||||
mockedState.detailJob = { ...jobs[0], isLoading: true };
|
||||
});
|
||||
|
||||
it('sets output on detail job', () => {
|
||||
mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' });
|
||||
mutations[types.RECEIVE_JOB_LOGS_SUCCESS](mockedState, { html: 'html' });
|
||||
expect(mockedState.detailJob.output).toBe('html');
|
||||
expect(mockedState.detailJob.isLoading).toBe(false);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
|
||||
|
||||
|
@ -21,9 +20,14 @@ describe('vue_shared/components/confirm_modal', () => {
|
|||
selector: '.test-button',
|
||||
};
|
||||
|
||||
const actionSpies = {
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
const popupMethods = {
|
||||
hide: jest.fn(),
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
||||
const GlModalStub = {
|
||||
template: '<div><slot></slot></div>',
|
||||
methods: popupMethods,
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
@ -34,8 +38,8 @@ describe('vue_shared/components/confirm_modal', () => {
|
|||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
methods: {
|
||||
...actionSpies,
|
||||
stubs: {
|
||||
GlModal: GlModalStub,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -44,7 +48,7 @@ describe('vue_shared/components/confirm_modal', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findModal = () => wrapper.find(GlModalStub);
|
||||
const findForm = () => wrapper.find('form');
|
||||
const findFormData = () =>
|
||||
findForm()
|
||||
|
@ -103,7 +107,7 @@ describe('vue_shared/components/confirm_modal', () => {
|
|||
});
|
||||
|
||||
it('does not close modal', () => {
|
||||
expect(actionSpies.closeModal).not.toHaveBeenCalled();
|
||||
expect(popupMethods.hide).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when modal closed', () => {
|
||||
|
@ -112,7 +116,7 @@ describe('vue_shared/components/confirm_modal', () => {
|
|||
});
|
||||
|
||||
it('closes modal', () => {
|
||||
expect(actionSpies.closeModal).toHaveBeenCalled();
|
||||
expect(popupMethods.hide).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,19 +14,13 @@ import {
|
|||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
function createRenamedComponent({
|
||||
props = {},
|
||||
methods = {},
|
||||
store = new Vuex.Store({}),
|
||||
deep = false,
|
||||
}) {
|
||||
function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) {
|
||||
const mnt = deep ? mount : shallowMount;
|
||||
|
||||
return mnt(Renamed, {
|
||||
propsData: { ...props },
|
||||
localVue,
|
||||
store,
|
||||
methods,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -258,25 +252,17 @@ describe('Renamed Diff Viewer', () => {
|
|||
'includes a link to the full file for alternate viewer type "$altType"',
|
||||
({ altType, linkText }) => {
|
||||
const file = { ...diffFile };
|
||||
const clickMock = jest.fn().mockImplementation(() => {});
|
||||
|
||||
file.alternate_viewer.name = altType;
|
||||
wrapper = createRenamedComponent({
|
||||
deep: true,
|
||||
props: { diffFile: file },
|
||||
methods: {
|
||||
clickLink: clickMock,
|
||||
},
|
||||
});
|
||||
|
||||
const link = wrapper.find('a');
|
||||
|
||||
expect(link.text()).toEqual(linkText);
|
||||
expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH);
|
||||
|
||||
link.vm.$emit('click');
|
||||
|
||||
expect(clickMock).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,17 +7,17 @@ RSpec.describe 'CI YML Templates' do
|
|||
|
||||
let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) }
|
||||
|
||||
let(:disabled_templates) do
|
||||
Gitlab::Template::GitlabCiYmlTemplate.disabled_templates.map do |template|
|
||||
template + Gitlab::Template::GitlabCiYmlTemplate.extension
|
||||
let(:excluded_templates) do
|
||||
all_templates.select do |name|
|
||||
Gitlab::Template::GitlabCiYmlTemplate.excluded_patterns.any? { |pattern| pattern.match?(name) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'included in a CI YAML configuration' do
|
||||
context 'when including available templates in a CI YAML configuration' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:template_name) do
|
||||
all_templates - disabled_templates
|
||||
all_templates - excluded_templates
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
@ -41,4 +41,29 @@ RSpec.describe 'CI YML Templates' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when including unavailable templates in a CI YAML configuration' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:template_name) do
|
||||
excluded_templates
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:content) do
|
||||
<<~EOS
|
||||
include:
|
||||
- template: #{template_name}
|
||||
|
||||
concrete_build_implemented_by_a_user:
|
||||
stage: test
|
||||
script: do something
|
||||
EOS
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,9 +15,9 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
|
|||
FileUtils.rm_rf(base_dir)
|
||||
end
|
||||
|
||||
subject(:finder) { described_class.new(base_dir, '', { 'General' => '', 'Bar' => 'Bar' }, exclusions: exclusions) }
|
||||
subject(:finder) { described_class.new(base_dir, '', { 'General' => '', 'Bar' => 'Bar' }, excluded_patterns: excluded_patterns) }
|
||||
|
||||
let(:exclusions) { [] }
|
||||
let(:excluded_patterns) { [] }
|
||||
|
||||
describe '.find' do
|
||||
context 'with a non-prefixed General template' do
|
||||
|
@ -38,7 +38,7 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
|
|||
end
|
||||
|
||||
context 'while listed as an exclusion' do
|
||||
let(:exclusions) { %w[test-template] }
|
||||
let(:excluded_patterns) { [%r{^test-template$}] }
|
||||
|
||||
it 'does not find the template without a prefix' do
|
||||
expect(finder.find('test-template')).to be_nil
|
||||
|
@ -77,7 +77,7 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
|
|||
end
|
||||
|
||||
context 'while listed as an exclusion' do
|
||||
let(:exclusions) { %w[Bar/test-template] }
|
||||
let(:excluded_patterns) { [%r{^Bar/test-template$}] }
|
||||
|
||||
it 'does not find the template with a prefix' do
|
||||
expect(finder.find('Bar/test-template')).to be_nil
|
||||
|
@ -96,6 +96,17 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
|
|||
expect(finder.find('Bar/test-template')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'while listed as an exclusion' do
|
||||
let(:excluded_patterns) { [%r{\.latest$}] }
|
||||
|
||||
it 'excludes the template matched the pattern' do
|
||||
create_template!('test-template.latest')
|
||||
|
||||
expect(finder.find('test-template')).to be_present
|
||||
expect(finder.find('test-template.latest')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,12 @@ RSpec.describe Gitlab::Template::GitlabCiYmlTemplate do
|
|||
expect(all).to include('Docker')
|
||||
expect(all).to include('Ruby')
|
||||
end
|
||||
|
||||
it 'does not include Browser-Performance template in FOSS' do
|
||||
all = subject.all.map(&:name)
|
||||
|
||||
expect(all).not_to include('Browser-Performance') unless Gitlab.ee?
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content' do
|
||||
|
|
|
@ -62,65 +62,81 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
end
|
||||
|
||||
describe '.track_event' do
|
||||
it "raise error if metrics don't have same aggregation" do
|
||||
expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
|
||||
context 'when usage_ping is disabled' do
|
||||
it 'does not track the event' do
|
||||
stub_application_setting(usage_ping_enabled: false)
|
||||
|
||||
described_class.track_event(entity1, weekly_event, Date.current)
|
||||
|
||||
expect(Gitlab::Redis::HLL).not_to receive(:add)
|
||||
end
|
||||
end
|
||||
|
||||
it 'raise error if metrics of unknown aggregation' do
|
||||
expect { described_class.track_event(entity1, 'unknown', Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
|
||||
end
|
||||
context 'when usage_ping is enabled' do
|
||||
before do
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
end
|
||||
|
||||
context 'for weekly events' do
|
||||
it 'sets the keys in Redis to expire automatically after the given expiry time' do
|
||||
described_class.track_event(entity1, "g_analytics_contribution")
|
||||
it "raise error if metrics don't have same aggregation" do
|
||||
expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
|
||||
end
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a
|
||||
expect(keys).not_to be_empty
|
||||
it 'raise error if metrics of unknown aggregation' do
|
||||
expect { described_class.track_event(entity1, 'unknown', Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
|
||||
end
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
|
||||
context 'for weekly events' do
|
||||
it 'sets the keys in Redis to expire automatically after the given expiry time' do
|
||||
described_class.track_event(entity1, "g_analytics_contribution")
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a
|
||||
expect(keys).not_to be_empty
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the keys in Redis to expire automatically after 6 weeks by default' do
|
||||
described_class.track_event(entity1, "g_compliance_dashboard")
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a
|
||||
expect(keys).not_to be_empty
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(6.weeks)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the keys in Redis to expire automatically after 6 weeks by default' do
|
||||
described_class.track_event(entity1, "g_compliance_dashboard")
|
||||
context 'for daily events' do
|
||||
it 'sets the keys in Redis to expire after the given expiry time' do
|
||||
described_class.track_event(entity1, "g_analytics_search")
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a
|
||||
expect(keys).not_to be_empty
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "*-g_{analytics}_search").to_a
|
||||
expect(keys).not_to be_empty
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(6.weeks)
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(84.days)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for daily events' do
|
||||
it 'sets the keys in Redis to expire after the given expiry time' do
|
||||
described_class.track_event(entity1, "g_analytics_search")
|
||||
it 'sets the keys in Redis to expire after 29 days by default' do
|
||||
described_class.track_event(entity1, "no_slot")
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "*-g_{analytics}_search").to_a
|
||||
expect(keys).not_to be_empty
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "*-{no_slot}").to_a
|
||||
expect(keys).not_to be_empty
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(84.days)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the keys in Redis to expire after 29 days by default' do
|
||||
described_class.track_event(entity1, "no_slot")
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
keys = redis.scan_each(match: "*-{no_slot}").to_a
|
||||
expect(keys).not_to be_empty
|
||||
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(29.days)
|
||||
keys.each do |key|
|
||||
expect(redis.ttl(key)).to be_within(5.seconds).of(29.days)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -304,30 +304,9 @@ RSpec.describe API::ProjectSnippets do
|
|||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
let(:snippet) { create(:project_snippet, :repository, author: admin, visibility_level: visibility_level, project: project) }
|
||||
|
||||
it 'updates snippet' do
|
||||
new_content = 'New content'
|
||||
new_description = 'New description'
|
||||
|
||||
update_snippet(params: { content: new_content, description: new_description, visibility: 'private' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
snippet.reload
|
||||
expect(snippet.content).to eq(new_content)
|
||||
expect(snippet.description).to eq(new_description)
|
||||
expect(snippet.visibility).to eq('private')
|
||||
end
|
||||
|
||||
it 'updates snippet with content parameter' do
|
||||
new_content = 'New content'
|
||||
new_description = 'New description'
|
||||
|
||||
update_snippet(params: { content: new_content, description: new_description })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
snippet.reload
|
||||
expect(snippet.content).to eq(new_content)
|
||||
expect(snippet.description).to eq(new_description)
|
||||
end
|
||||
it_behaves_like 'snippet file updates'
|
||||
it_behaves_like 'snippet non-file updates'
|
||||
it_behaves_like 'invalid snippet updates'
|
||||
|
||||
it 'updates snippet with visibility parameter' do
|
||||
expect { update_snippet(params: { visibility: 'private' }) }
|
||||
|
@ -336,33 +315,6 @@ RSpec.describe API::ProjectSnippets do
|
|||
expect(snippet.visibility).to eq('private')
|
||||
end
|
||||
|
||||
it 'returns 404 for invalid snippet id' do
|
||||
update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
update_snippet
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq 'title, file_name, content, visibility are missing, at least one parameter must be provided'
|
||||
end
|
||||
|
||||
it 'returns 400 if content is blank' do
|
||||
update_snippet(params: { content: '' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 if title is blank' do
|
||||
update_snippet(params: { title: '' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq 'title is empty'
|
||||
end
|
||||
|
||||
it_behaves_like 'update with repository actions' do
|
||||
let(:snippet_without_repo) { create(:project_snippet, author: admin, project: project, visibility_level: visibility_level) }
|
||||
end
|
||||
|
|
|
@ -368,7 +368,7 @@ RSpec.describe API::Snippets do
|
|||
context 'when the snippet is public' do
|
||||
let(:extra_params) { { visibility: 'public' } }
|
||||
|
||||
it 'rejects the shippet' do
|
||||
it 'rejects the snippet' do
|
||||
expect { subject }.not_to change { Snippet.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
@ -391,97 +391,16 @@ RSpec.describe API::Snippets do
|
|||
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
|
||||
end
|
||||
|
||||
let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
|
||||
let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
|
||||
let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
|
||||
let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
|
||||
let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
|
||||
let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
|
||||
let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
|
||||
|
||||
context 'with snippet file changes' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:is_multi_file, :file_name, :content, :files, :status) do
|
||||
true | nil | nil | [create_action] | :success
|
||||
true | nil | nil | [update_action] | :success
|
||||
true | nil | nil | [move_action] | :success
|
||||
true | nil | nil | [delete_action] | :success
|
||||
true | nil | nil | [create_action, update_action] | :success
|
||||
true | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||
true | 'foo.txt' | 'bar' | nil | :bad_request
|
||||
true | nil | nil | nil | :bad_request
|
||||
true | 'foo.txt' | nil | [create_action] | :bad_request
|
||||
true | nil | 'bar' | [create_action] | :bad_request
|
||||
true | '' | nil | [create_action] | :bad_request
|
||||
true | nil | '' | [create_action] | :bad_request
|
||||
true | nil | nil | [bad_file_path] | :bad_request
|
||||
true | nil | nil | [bad_previous_path] | :bad_request
|
||||
true | nil | nil | [invalid_move] | :unprocessable_entity
|
||||
|
||||
false | 'foo.txt' | 'bar' | nil | :success
|
||||
false | 'foo.txt' | nil | nil | :success
|
||||
false | nil | 'bar' | nil | :success
|
||||
false | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||
false | nil | nil | nil | :bad_request
|
||||
false | nil | '' | nil | :bad_request
|
||||
false | nil | nil | [bad_file_path] | :bad_request
|
||||
false | nil | nil | [bad_previous_path] | :bad_request
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
|
||||
end
|
||||
|
||||
it 'has the correct response' do
|
||||
update_params = {}.tap do |params|
|
||||
params[:files] = files if files
|
||||
params[:file_name] = file_name if file_name
|
||||
params[:content] = content if content
|
||||
end
|
||||
|
||||
update_snippet(params: update_params)
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save fails due to a repository commit error' do
|
||||
before do
|
||||
allow_next_instance_of(Repository) do |instance|
|
||||
allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
|
||||
end
|
||||
|
||||
update_snippet(params: { files: [create_action] })
|
||||
end
|
||||
|
||||
it 'returns a bad request response' do
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'snippet non-file updates' do
|
||||
it 'updates a snippet non-file attributes' do
|
||||
new_description = 'New description'
|
||||
new_title = 'New title'
|
||||
new_visibility = 'internal'
|
||||
|
||||
update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
|
||||
|
||||
snippet.reload
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(snippet.description).to eq(new_description)
|
||||
expect(snippet.visibility).to eq(new_visibility)
|
||||
expect(snippet.title).to eq(new_title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet file updates'
|
||||
it_behaves_like 'snippet non-file updates'
|
||||
it_behaves_like 'invalid snippet updates'
|
||||
|
||||
it "returns 404 for another user's snippet" do
|
||||
update_snippet(requester: other_user, params: { title: 'foobar' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
context 'with restricted visibility settings' do
|
||||
before do
|
||||
|
@ -493,33 +412,6 @@ RSpec.describe API::Snippets do
|
|||
it_behaves_like 'snippet non-file updates'
|
||||
end
|
||||
|
||||
it 'returns 404 for invalid snippet id' do
|
||||
update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it "returns 404 for another user's snippet" do
|
||||
update_snippet(requester: other_user, params: { title: 'foobar' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
update_snippet
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 if title is blank' do
|
||||
update_snippet(params: { title: '' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq 'title is empty'
|
||||
end
|
||||
|
||||
it_behaves_like 'update with repository actions' do
|
||||
let(:snippet_without_repo) { create(:personal_snippet, author: user, visibility_level: visibility_level) }
|
||||
end
|
||||
|
@ -543,7 +435,7 @@ RSpec.describe API::Snippets do
|
|||
context 'when the snippet is public' do
|
||||
let(:visibility_level) { Snippet::PUBLIC }
|
||||
|
||||
it 'rejects the shippet' do
|
||||
it 'rejects the snippet' do
|
||||
expect { update_snippet(params: { title: 'Foo' }) }
|
||||
.not_to change { snippet.reload.title }
|
||||
|
||||
|
|
|
@ -37,10 +37,7 @@ module StubbedFeature
|
|||
# We do `m.call` as we want to validate the execution of method arguments
|
||||
# and a feature flag state if it is not persisted
|
||||
unless Feature.persisted_name?(args.first)
|
||||
# TODO: this is hack to support `promo_feature_available?`
|
||||
# We enable all feature flags by default unless they are `promo_`
|
||||
# Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667
|
||||
feature_flag = true unless args.first.to_s.start_with?('promo_')
|
||||
feature_flag = true
|
||||
end
|
||||
|
||||
feature_flag
|
||||
|
|
|
@ -77,3 +77,123 @@ RSpec.shared_examples 'raw snippet files' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'snippet file updates' do
|
||||
let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
|
||||
let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
|
||||
let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
|
||||
let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
|
||||
let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
|
||||
let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
|
||||
let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
|
||||
|
||||
context 'with various snippet file changes' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:is_multi_file, :file_name, :content, :files, :status) do
|
||||
true | nil | nil | [create_action] | :success
|
||||
true | nil | nil | [update_action] | :success
|
||||
true | nil | nil | [move_action] | :success
|
||||
true | nil | nil | [delete_action] | :success
|
||||
true | nil | nil | [create_action, update_action] | :success
|
||||
true | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||
true | 'foo.txt' | 'bar' | nil | :bad_request
|
||||
true | nil | nil | nil | :bad_request
|
||||
true | 'foo.txt' | nil | [create_action] | :bad_request
|
||||
true | nil | 'bar' | [create_action] | :bad_request
|
||||
true | '' | nil | [create_action] | :bad_request
|
||||
true | nil | '' | [create_action] | :bad_request
|
||||
true | nil | nil | [bad_file_path] | :bad_request
|
||||
true | nil | nil | [bad_previous_path] | :bad_request
|
||||
true | nil | nil | [invalid_move] | :unprocessable_entity
|
||||
|
||||
false | 'foo.txt' | 'bar' | nil | :success
|
||||
false | 'foo.txt' | nil | nil | :success
|
||||
false | nil | 'bar' | nil | :success
|
||||
false | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||
false | nil | nil | nil | :bad_request
|
||||
false | nil | '' | nil | :bad_request
|
||||
false | nil | nil | [bad_file_path] | :bad_request
|
||||
false | nil | nil | [bad_previous_path] | :bad_request
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
|
||||
end
|
||||
|
||||
it 'has the correct response' do
|
||||
update_params = {}.tap do |params|
|
||||
params[:files] = files if files
|
||||
params[:file_name] = file_name if file_name
|
||||
params[:content] = content if content
|
||||
end
|
||||
|
||||
update_snippet(params: update_params)
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when save fails due to a repository commit error' do
|
||||
before do
|
||||
allow_next_instance_of(Repository) do |instance|
|
||||
allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
|
||||
end
|
||||
|
||||
update_snippet(params: { files: [create_action] })
|
||||
end
|
||||
|
||||
it 'returns a bad request response' do
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'snippet non-file updates' do
|
||||
it 'updates a snippet non-file attributes' do
|
||||
new_description = 'New description'
|
||||
new_title = 'New title'
|
||||
new_visibility = 'internal'
|
||||
|
||||
update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
|
||||
|
||||
snippet.reload
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(snippet.description).to eq(new_description)
|
||||
expect(snippet.visibility).to eq(new_visibility)
|
||||
expect(snippet.title).to eq(new_title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'invalid snippet updates' do
|
||||
it 'returns 404 for invalid snippet id' do
|
||||
update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Snippet Not Found')
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
update_snippet
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 if content is blank' do
|
||||
update_snippet(params: { content: '' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 if title is blank' do
|
||||
update_snippet(params: { title: '' })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq 'title is empty'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue