Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
23b60ed2c1
commit
b4e7d9d839
77 changed files with 590 additions and 237 deletions
|
@ -2,6 +2,14 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 14.0.1 (2021-06-24)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
||||
- [Remove add button from Devops Adoption](gitlab-org/gitlab@1c60bdf5daf64f10f001eeb5134f08a53a148d90) ([merge request](gitlab-org/gitlab!64764)) **GitLab Enterprise Edition**
|
||||
- [DevOps Adoption - ensure displayNamespaceId is included](gitlab-org/gitlab@9eb7cd5212cfc19f4cd6578c8e4afc7b4da27eab) ([merge request](gitlab-org/gitlab!64764)) **GitLab Enterprise Edition**
|
||||
- [Add Helm-2to3.gitlab-ci.yml to Auto DevOps](gitlab-org/gitlab@61ac7f46b06fcf151be62407dc0837a44843800e) ([merge request](gitlab-org/gitlab!64764))
|
||||
|
||||
## 14.0.0 (2021-06-21)
|
||||
|
||||
### Added (116 changes)
|
||||
|
|
|
@ -43,14 +43,22 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
|
||||
const { href } = editor.getAttributes(linkContentType);
|
||||
const { 'data-canonical-src': canonicalSrc, href } = editor.getAttributes(linkContentType);
|
||||
|
||||
this.linkHref = href;
|
||||
this.linkHref = canonicalSrc || href;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
updateLink() {
|
||||
this.tiptapEditor.chain().focus().unsetLink().setLink({ href: this.linkHref }).run();
|
||||
this.tiptapEditor
|
||||
.chain()
|
||||
.focus()
|
||||
.unsetLink()
|
||||
.setLink({
|
||||
href: this.linkHref,
|
||||
'data-canonical-src': this.linkHref,
|
||||
})
|
||||
.run();
|
||||
|
||||
this.$emit('execute', { contentType: linkContentType });
|
||||
},
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { markInputRule } from '@tiptap/core';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
|
||||
|
||||
export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
|
||||
|
||||
export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
|
||||
|
||||
const extractHrefFromMatch = (match) => {
|
||||
|
@ -29,8 +27,37 @@ export const tiptapExtension = Link.extend({
|
|||
markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch),
|
||||
];
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
href: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
return {
|
||||
href: element.getAttribute('href'),
|
||||
};
|
||||
},
|
||||
},
|
||||
'data-canonical-src': {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
return {
|
||||
href: element.dataset.canonicalSrc,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}).configure({
|
||||
openOnClick: false,
|
||||
});
|
||||
|
||||
export const serializer = defaultMarkdownSerializer.marks.link;
|
||||
export const serializer = {
|
||||
open() {
|
||||
return '[';
|
||||
},
|
||||
close(state, mark) {
|
||||
const href = mark.attrs['data-canonical-src'] || mark.attrs.href;
|
||||
return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -48,7 +48,6 @@ export const receiveSettingsError = ({ commit }, { response = {} }) => {
|
|||
|
||||
createFlash({
|
||||
message: `${__('There was an error saving your changes.')} ${message}`,
|
||||
type: 'alert',
|
||||
});
|
||||
commit(types.UPDATE_SETTINGS_LOADING, false);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import { mergeUrlParams } from '../lib/utils/url_utility';
|
||||
import DropdownAjaxFilter from './dropdown_ajax_filter';
|
||||
import DropdownEmoji from './dropdown_emoji';
|
||||
|
@ -87,6 +88,7 @@ export default class AvailableDropdownMappings {
|
|||
extraArguments: {
|
||||
endpoint: this.getMilestoneEndpoint(),
|
||||
symbol: '%',
|
||||
preprocessing: (milestones) => milestones.sort(sortMilestonesByDueDate),
|
||||
},
|
||||
element: this.container.querySelector('#js-dropdown-milestone'),
|
||||
},
|
||||
|
|
|
@ -40,6 +40,5 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
|
|||
|
||||
createFlash({
|
||||
message: `${__('There was an error saving your changes.')} ${message}`,
|
||||
type: 'alert',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -61,9 +61,6 @@ export default {
|
|||
message: sprintf(s__('The name "%{name}" is already taken in this directory.'), {
|
||||
name: this.entryName,
|
||||
}),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -252,9 +252,6 @@ export default {
|
|||
.catch((err) => {
|
||||
createFlash({
|
||||
message: __('Error setting up editor. Please try again.'),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -113,9 +113,6 @@ export const createRouter = (store, defaultBranch) => {
|
|||
.catch((e) => {
|
||||
createFlash({
|
||||
message: __('Error while loading the project data. Please try again.'),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -100,7 +100,7 @@ export default {
|
|||
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
|
||||
},
|
||||
pingUsage(projectPath) {
|
||||
const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`;
|
||||
const url = `${gon.relative_url_root}/${projectPath}/service_ping/web_ide_pipelines_count`;
|
||||
return axios.post(url);
|
||||
},
|
||||
getCiConfig(projectPath, content) {
|
||||
|
|
|
@ -40,10 +40,6 @@ export const createTempEntry = (
|
|||
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
|
||||
name: name.split('/').pop(),
|
||||
}),
|
||||
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
@ -287,9 +283,6 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
|
|||
} else {
|
||||
createFlash({
|
||||
message: __('Error loading branch data. Please try again.'),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -36,9 +36,6 @@ export const getMergeRequestsForBranch = (
|
|||
.catch((e) => {
|
||||
createFlash({
|
||||
message: __(`Error fetching merge requests for ${branchId}`),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -21,9 +21,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
|
|||
.catch(() => {
|
||||
createFlash({
|
||||
message: __('Error loading project data. Please try again.'),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
@ -47,9 +44,6 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
|
|||
.catch((e) => {
|
||||
createFlash({
|
||||
message: __('Error loading last commit.'),
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
export const pingUsage = ({ rootGetters }) => {
|
||||
const { web_url: projectUrl } = rootGetters.currentProject;
|
||||
|
||||
const url = `${projectUrl}/usage_ping/web_ide_clientside_preview`;
|
||||
const url = `${projectUrl}/service_ping/web_ide_clientside_preview`;
|
||||
|
||||
return axios.post(url);
|
||||
};
|
||||
|
|
|
@ -145,9 +145,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
|
|||
if (!data.short_id) {
|
||||
createFlash({
|
||||
message: data.message,
|
||||
type: 'alert',
|
||||
parent: document,
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -24,7 +24,6 @@ export default class IncidentsSettingsService {
|
|||
|
||||
createFlash({
|
||||
message: `${ERROR_MSG} ${message}`,
|
||||
type: 'alert',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { template, escape } from 'lodash';
|
|||
import Api from '~/api';
|
||||
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
import boardsStore, {
|
||||
boardStoreIssueSet,
|
||||
boardStoreIssueDelete,
|
||||
|
@ -93,21 +94,7 @@ export default class MilestoneSelect {
|
|||
// Public API includes `title` instead of `name`.
|
||||
name: m.title,
|
||||
}))
|
||||
.sort((mA, mB) => {
|
||||
const dueDateA = mA.due_date ? parsePikadayDate(mA.due_date) : null;
|
||||
const dueDateB = mB.due_date ? parsePikadayDate(mB.due_date) : null;
|
||||
|
||||
// Move all expired milestones to the bottom.
|
||||
if (mA.expired) return 1;
|
||||
if (mB.expired) return -1;
|
||||
|
||||
// Move milestones without due dates just above expired milestones.
|
||||
if (!dueDateA) return 1;
|
||||
if (!dueDateB) return -1;
|
||||
|
||||
// Sort by due date in ascending order.
|
||||
return dueDateA - dueDateB;
|
||||
}),
|
||||
.sort(sortMilestonesByDueDate),
|
||||
)
|
||||
.then((data) => {
|
||||
const extraOptions = [];
|
||||
|
|
32
app/assets/javascripts/milestones/milestone_utils.js
Normal file
32
app/assets/javascripts/milestones/milestone_utils.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { parsePikadayDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
/**
|
||||
* This method is to be used with `Array.prototype.sort` function
|
||||
* where array contains milestones with `due_date`/`dueDate` and/or
|
||||
* `expired` properties.
|
||||
* This method sorts given milestone params based on their expiration
|
||||
* status by putting expired milestones at the bottom and upcoming
|
||||
* milestones at the top of the list.
|
||||
*
|
||||
* @param {object} milestoneA
|
||||
* @param {object} milestoneB
|
||||
*/
|
||||
export function sortMilestonesByDueDate(milestoneA, milestoneB) {
|
||||
const rawDueDateA = milestoneA.due_date || milestoneA.dueDate;
|
||||
const rawDueDateB = milestoneB.due_date || milestoneB.dueDate;
|
||||
const dueDateA = rawDueDateA ? parsePikadayDate(rawDueDateA) : null;
|
||||
const dueDateB = rawDueDateB ? parsePikadayDate(rawDueDateB) : null;
|
||||
const expiredA = milestoneA.expired || Date.now() > dueDateA?.getTime();
|
||||
const expiredB = milestoneB.expired || Date.now() > dueDateB?.getTime();
|
||||
|
||||
// Move all expired milestones to the bottom.
|
||||
if (expiredA) return 1;
|
||||
if (expiredB) return -1;
|
||||
|
||||
// Move milestones without due dates just above expired milestones.
|
||||
if (!dueDateA) return 1;
|
||||
if (!dueDateB) return -1;
|
||||
|
||||
// Sort by due date in ascending order.
|
||||
return dueDateA - dueDateB;
|
||||
}
|
|
@ -628,7 +628,6 @@ export default class Notes {
|
|||
message: __(
|
||||
'Your comment could not be submitted! Please check your network connection and try again.',
|
||||
),
|
||||
type: 'alert',
|
||||
parent: formParentTimeline.get(0),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -222,7 +222,6 @@ export default {
|
|||
);
|
||||
createFlash({
|
||||
message: msg,
|
||||
type: 'alert',
|
||||
parent: this.$el,
|
||||
});
|
||||
this.$refs.noteForm.note = noteText;
|
||||
|
|
|
@ -320,7 +320,6 @@ export default {
|
|||
const msg = __('Something went wrong while editing your comment. Please try again.');
|
||||
createFlash({
|
||||
message: msg,
|
||||
type: 'alert',
|
||||
parent: this.$el,
|
||||
});
|
||||
this.recoverNoteContent(noteText);
|
||||
|
|
|
@ -48,7 +48,6 @@ export default {
|
|||
const msg = __('Something went wrong while resolving this discussion. Please try again.');
|
||||
createFlash({
|
||||
message: msg,
|
||||
type: 'alert',
|
||||
parent: this.$el,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -381,7 +381,6 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
.catch(() => {
|
||||
createFlash({
|
||||
message: __('Something went wrong while adding your award. Please try again.'),
|
||||
type: 'alert',
|
||||
parent: noteData.flashContainer,
|
||||
});
|
||||
})
|
||||
|
@ -423,7 +422,6 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
});
|
||||
createFlash({
|
||||
message: errorMsg,
|
||||
type: 'alert',
|
||||
parent: noteData.flashContainer,
|
||||
});
|
||||
return { ...data, hasFlash: true };
|
||||
|
@ -627,7 +625,6 @@ export const submitSuggestion = (
|
|||
|
||||
createFlash({
|
||||
message: __(flashMessage),
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
})
|
||||
|
@ -664,7 +661,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
|
|||
|
||||
createFlash({
|
||||
message: __(flashMessage),
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
})
|
||||
|
|
|
@ -37,6 +37,5 @@ export const receiveSaveChangesError = (_, error) => {
|
|||
|
||||
createFlash({
|
||||
message: `${__('There was an error saving your changes.')} ${message}`,
|
||||
type: 'alert',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -108,7 +108,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
isTimeTrackingInfoLoading() {
|
||||
return this.$apollo?.queries.issuableTimeTracking.loading ?? false;
|
||||
return this.$apollo?.queries.issuableTimeTracking?.loading ?? false;
|
||||
},
|
||||
timeEstimate() {
|
||||
return this.timeTracking?.timeEstimate || 0;
|
||||
|
|
|
@ -24,7 +24,6 @@ export default class TaskList {
|
|||
|
||||
return createFlash({
|
||||
message: errorMessages || __('Update failed'),
|
||||
type: 'alert',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -64,12 +64,6 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<span :class="cssClass">
|
||||
<gl-icon
|
||||
:name="icon"
|
||||
:size="size"
|
||||
:class="cssClasses"
|
||||
:aria-label="status.icon"
|
||||
use-deprecated-sizes
|
||||
/>
|
||||
<gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { debounce } from 'lodash';
|
|||
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
|
||||
import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
|
||||
import { stripQuotes } from '../filtered_search_utils';
|
||||
|
@ -63,7 +64,7 @@ export default {
|
|||
this.config
|
||||
.fetchMilestones(searchTerm)
|
||||
.then(({ data }) => {
|
||||
this.milestones = data;
|
||||
this.milestones = data.sort(sortMilestonesByDueDate);
|
||||
})
|
||||
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
|
||||
.finally(() => {
|
||||
|
|
|
@ -81,7 +81,6 @@ export default {
|
|||
if (this.lineType === 'old') {
|
||||
createFlash({
|
||||
message: __('Unable to apply suggestions to a deleted line.'),
|
||||
type: 'alert',
|
||||
parent: this.$el,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
|
|||
@milestones = milestones.page(params[:page])
|
||||
end
|
||||
format.json do
|
||||
render json: milestones.to_json(only: [:id, :title], methods: :name)
|
||||
render json: milestones.to_json(only: [:id, :title, :due_date], methods: :name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
|||
@milestones = milestones.page(params[:page])
|
||||
end
|
||||
format.json do
|
||||
render json: milestones.to_json(only: [:id, :title], methods: :name)
|
||||
render json: milestones.to_json(only: [:id, :title, :due_date], methods: :name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
|||
@milestones = @milestones.page(params[:page])
|
||||
end
|
||||
format.json do
|
||||
render json: @milestones.to_json(only: [:id, :title], methods: :name)
|
||||
render json: @milestones.to_json(only: [:id, :title, :due_date], methods: :name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,9 +54,13 @@ module Projects
|
|||
end
|
||||
|
||||
def set_index_vars
|
||||
# Loading project members so that we can fetch access level of the bot
|
||||
# user in the project without multiple queries.
|
||||
@project.project_members.load
|
||||
|
||||
@scopes = Gitlab::Auth.resource_bot_scopes
|
||||
@active_project_access_tokens = finder(state: 'active').execute
|
||||
@inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute
|
||||
@active_project_access_tokens = finder(state: 'active').execute.preload_users
|
||||
@inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
|
||||
@new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
|
||||
end
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ module Types
|
|||
field :short_sha, type: GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Short SHA1 ID of the commit.'
|
||||
field :scheduling_type, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Type of pipeline scheduling. Value is `dag` if the pipeline uses the `needs` keyword, and `stage` otherwise.'
|
||||
description: 'Type of job scheduling. Value is `dag` if the job uses the `needs` keyword, and `stage` otherwise.'
|
||||
field :commit_path, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Path to the commit that triggered the job.'
|
||||
field :ref_name, GraphQL::STRING_TYPE, null: true,
|
||||
|
|
|
@ -57,6 +57,7 @@ module Types
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def jobs_for_pipeline(pipeline, stage_ids, include_needs)
|
||||
results = pipeline.latest_statuses.where(stage_id: stage_ids)
|
||||
results = results.preload(:project)
|
||||
results = results.preload(:needs) if include_needs
|
||||
|
||||
results.group_by(&:stage_id)
|
||||
|
|
|
@ -121,7 +121,7 @@ module Ci
|
|||
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
|
||||
return if offset == size # Skip the following process as it doesn't affect anything
|
||||
|
||||
self.append("", offset)
|
||||
self.append(+"", offset)
|
||||
end
|
||||
|
||||
def append(new_data, offset)
|
||||
|
|
|
@ -25,10 +25,18 @@ module Ci
|
|||
files.create(create_attributes(model, new_data))
|
||||
end
|
||||
|
||||
# This is the sequence that causes append_data to be called:
|
||||
#
|
||||
# 1. Runner sends a PUT /api/v4/jobs/:id to indicate the job is canceled or finished.
|
||||
# 2. UpdateBuildStateService#accept_build_state! persists all live job logs to object storage (or filesystem).
|
||||
# 3. UpdateBuildStateService#accept_build_state! returns a 202 to the runner.
|
||||
# 4. The runner continues to send PATCH requests with job logs until all logs have been sent and received.
|
||||
# 5. If the last PATCH request arrives after the job log has been persisted, we
|
||||
# retrieve the data from object storage to append the remaining lines.
|
||||
def append_data(model, new_data, offset)
|
||||
if offset > 0
|
||||
truncated_data = data(model).to_s.byteslice(0, offset)
|
||||
new_data = truncated_data + new_data
|
||||
new_data = append_strings(truncated_data, new_data)
|
||||
end
|
||||
|
||||
set_data(model, new_data)
|
||||
|
@ -71,6 +79,17 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def append_strings(old_data, new_data)
|
||||
if Feature.enabled?(:ci_job_trace_force_encode, default_enabled: :yaml)
|
||||
# When object storage is in use, old_data may be retrieved in UTF-8.
|
||||
old_data = old_data.force_encoding(Encoding::ASCII_8BIT)
|
||||
# new_data should already be in ASCII-8BIT, but just in case it isn't, do this.
|
||||
new_data = new_data.force_encoding(Encoding::ASCII_8BIT)
|
||||
end
|
||||
|
||||
old_data + new_data
|
||||
end
|
||||
|
||||
def key(model)
|
||||
key_raw(model.build_id, model.chunk_index)
|
||||
end
|
||||
|
|
|
@ -176,7 +176,6 @@ class WebHookService
|
|||
end
|
||||
|
||||
def rate_limited?
|
||||
return false unless Feature.enabled?(:web_hooks_rate_limit, default_enabled: :yaml)
|
||||
return false if rate_limit.nil?
|
||||
|
||||
Gitlab::ApplicationRateLimiter.throttled?(
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
= render 'shared/access_tokens/table',
|
||||
active_tokens: @active_project_access_tokens,
|
||||
project: @project,
|
||||
type: type,
|
||||
type_plural: type_plural,
|
||||
revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
|
||||
- impersonation = local_assigns.fetch(:impersonation, false)
|
||||
- project = local_assigns.fetch(:project, false)
|
||||
|
||||
%hr
|
||||
|
||||
|
@ -20,6 +21,8 @@
|
|||
= _('Last Used')
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'view-the-last-time-a-token-was-used'), target: '_blank'
|
||||
%th= _('Expires')
|
||||
- if project
|
||||
%th= _('Role')
|
||||
%th= _('Scopes')
|
||||
%th
|
||||
%tbody
|
||||
|
@ -42,6 +45,8 @@
|
|||
= _('In %{time_to_now}') % { time_to_now: distance_of_time_in_words_to_now(token.expires_at) }
|
||||
- else
|
||||
%span.token-never-expires-label= _('Never')
|
||||
- if project
|
||||
%td= project.project_member(token.user).human_access
|
||||
%td= token.scopes.present? ? token.scopes.join(', ') : _('no scopes selected')
|
||||
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'gl-button btn btn-danger btn-sm float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } }
|
||||
- else
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: web_hooks_rate_limit
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61151
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330133
|
||||
milestone: '13.12'
|
||||
name: ci_job_trace_force_encode
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64631
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333452
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::verify
|
||||
default_enabled: false
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveClustersApplicationsFluentdTable < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
drop_table :clusters_applications_fluentd
|
||||
end
|
||||
|
||||
def down
|
||||
create_table :clusters_applications_fluentd do |t|
|
||||
t.integer :protocol, null: false, limit: 2
|
||||
t.integer :status, null: false
|
||||
t.integer :port, null: false
|
||||
t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
|
||||
t.timestamps_with_timezone null: false
|
||||
t.string :version, null: false, limit: 255
|
||||
t.string :host, null: false, limit: 255
|
||||
t.boolean :cilium_log_enabled, default: true, null: false
|
||||
t.boolean :waf_log_enabled, default: true, null: false
|
||||
t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
|
||||
end
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210610042700
Normal file
1
db/schema_migrations/20210610042700
Normal file
|
@ -0,0 +1 @@
|
|||
f8b8276ed7e120b61f6748a328590a98f0e444e0d26bcb1a2b0daa54c3643acd
|
|
@ -11633,30 +11633,6 @@ CREATE SEQUENCE clusters_applications_elastic_stacks_id_seq
|
|||
|
||||
ALTER SEQUENCE clusters_applications_elastic_stacks_id_seq OWNED BY clusters_applications_elastic_stacks.id;
|
||||
|
||||
CREATE TABLE clusters_applications_fluentd (
|
||||
id bigint NOT NULL,
|
||||
protocol smallint NOT NULL,
|
||||
status integer NOT NULL,
|
||||
port integer NOT NULL,
|
||||
cluster_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
version character varying(255) NOT NULL,
|
||||
host character varying(255) NOT NULL,
|
||||
status_reason text,
|
||||
waf_log_enabled boolean DEFAULT true NOT NULL,
|
||||
cilium_log_enabled boolean DEFAULT true NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE clusters_applications_fluentd_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE clusters_applications_fluentd_id_seq OWNED BY clusters_applications_fluentd.id;
|
||||
|
||||
CREATE TABLE clusters_applications_helm (
|
||||
id integer NOT NULL,
|
||||
cluster_id integer NOT NULL,
|
||||
|
@ -19817,8 +19793,6 @@ ALTER TABLE ONLY clusters_applications_crossplane ALTER COLUMN id SET DEFAULT ne
|
|||
|
||||
ALTER TABLE ONLY clusters_applications_elastic_stacks ALTER COLUMN id SET DEFAULT nextval('clusters_applications_elastic_stacks_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_fluentd ALTER COLUMN id SET DEFAULT nextval('clusters_applications_fluentd_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_helm ALTER COLUMN id SET DEFAULT nextval('clusters_applications_helm_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_ingress ALTER COLUMN id SET DEFAULT nextval('clusters_applications_ingress_id_seq'::regclass);
|
||||
|
@ -21049,9 +21023,6 @@ ALTER TABLE ONLY clusters_applications_crossplane
|
|||
ALTER TABLE ONLY clusters_applications_elastic_stacks
|
||||
ADD CONSTRAINT clusters_applications_elastic_stacks_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_fluentd
|
||||
ADD CONSTRAINT clusters_applications_fluentd_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_helm
|
||||
ADD CONSTRAINT clusters_applications_helm_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -23100,8 +23071,6 @@ CREATE UNIQUE INDEX index_clusters_applications_crossplane_on_cluster_id ON clus
|
|||
|
||||
CREATE UNIQUE INDEX index_clusters_applications_elastic_stacks_on_cluster_id ON clusters_applications_elastic_stacks USING btree (cluster_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_clusters_applications_fluentd_on_cluster_id ON clusters_applications_fluentd USING btree (cluster_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_clusters_applications_helm_on_cluster_id ON clusters_applications_helm USING btree (cluster_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_clusters_applications_ingress_on_cluster_id ON clusters_applications_ingress USING btree (cluster_id);
|
||||
|
@ -26707,9 +26676,6 @@ ALTER TABLE ONLY security_orchestration_policy_configurations
|
|||
ALTER TABLE ONLY ci_resources
|
||||
ADD CONSTRAINT fk_rails_430336af2d FOREIGN KEY (resource_group_id) REFERENCES ci_resource_groups(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY clusters_applications_fluentd
|
||||
ADD CONSTRAINT fk_rails_4319b1dcd2 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY batched_background_migration_jobs
|
||||
ADD CONSTRAINT fk_rails_432153b86d FOREIGN KEY (batched_background_migration_id) REFERENCES batched_background_migrations(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -115,10 +115,7 @@ Limit the maximum daily member invitations allowed per group hierarchy.
|
|||
### Webhook rate limit
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61151) in GitLab 13.12.
|
||||
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
|
||||
> - Disabled on GitLab.com.
|
||||
> - Not recommended for production use.
|
||||
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-rate-limiting-for-webhooks). **(FREE SELF)**
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/330133) in GitLab 14.1.
|
||||
|
||||
Limit the number of times any given webhook can be called per minute.
|
||||
This only applies to project and group webhooks.
|
||||
|
@ -136,25 +133,6 @@ Set the limit to `0` to disable it.
|
|||
|
||||
- **Default rate limit**: Disabled.
|
||||
|
||||
#### Enable or disable rate limiting for webhooks **(FREE SELF)**
|
||||
|
||||
Rate limiting for webhooks is under development and not 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(:web_hooks_rate_limit)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:web_hooks_rate_limit)
|
||||
```
|
||||
|
||||
## Gitaly concurrency limit
|
||||
|
||||
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly's configuration file.
|
||||
|
|
|
@ -7589,7 +7589,7 @@ Represents the total number of issues and their weights for a particular day.
|
|||
| <a id="cijobrefpath"></a>`refPath` | [`String`](#string) | Path to the ref. |
|
||||
| <a id="cijobretryable"></a>`retryable` | [`Boolean!`](#boolean) | Indicates the job can be retried. |
|
||||
| <a id="cijobscheduledat"></a>`scheduledAt` | [`Time`](#time) | Schedule for the build. |
|
||||
| <a id="cijobschedulingtype"></a>`schedulingType` | [`String`](#string) | Type of pipeline scheduling. Value is `dag` if the pipeline uses the `needs` keyword, and `stage` otherwise. |
|
||||
| <a id="cijobschedulingtype"></a>`schedulingType` | [`String`](#string) | Type of job scheduling. Value is `dag` if the job uses the `needs` keyword, and `stage` otherwise. |
|
||||
| <a id="cijobshortsha"></a>`shortSha` | [`String!`](#string) | Short SHA1 ID of the commit. |
|
||||
| <a id="cijobstage"></a>`stage` | [`CiStage`](#cistage) | Stage of the job. |
|
||||
| <a id="cijobstartedat"></a>`startedAt` | [`Time`](#time) | When the job was started. |
|
||||
|
|
|
@ -38,7 +38,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
"id" : 42,
|
||||
"active" : true,
|
||||
"created_at" : "2021-01-20T22:11:48.151Z",
|
||||
"revoked" : false
|
||||
"revoked" : false,
|
||||
"access_level": 40
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -80,7 +81,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
"user_id" : 166,
|
||||
"id" : 58,
|
||||
"expires_at" : "2021-01-31",
|
||||
"token" : "D4y...Wzr"
|
||||
"token" : "D4y...Wzr",
|
||||
"access_level": 40
|
||||
}
|
||||
```
|
||||
|
||||
|
|
11
lib/api/entities/resource_access_token.rb
Normal file
11
lib/api/entities/resource_access_token.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ResourceAccessToken < Entities::PersonalAccessToken
|
||||
expose :access_level do |token, options|
|
||||
options[:project].project_member(token.user).access_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
lib/api/entities/resource_access_token_with_token.rb
Normal file
9
lib/api/entities/resource_access_token_with_token.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ResourceAccessTokenWithToken < Entities::ResourceAccessToken
|
||||
expose :token
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,9 +21,10 @@ module API
|
|||
|
||||
next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource)
|
||||
|
||||
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute
|
||||
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users
|
||||
|
||||
present paginate(tokens), with: Entities::PersonalAccessToken
|
||||
resource.project_members.load
|
||||
present paginate(tokens), with: Entities::ResourceAccessToken, project: resource
|
||||
end
|
||||
|
||||
desc 'Revoke a resource access token' do
|
||||
|
@ -69,7 +70,7 @@ module API
|
|||
).execute
|
||||
|
||||
if token_response.success?
|
||||
present token_response.payload[:access_token], with: Entities::PersonalAccessTokenWithToken
|
||||
present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, project: resource
|
||||
else
|
||||
bad_request!(token_response.message)
|
||||
end
|
||||
|
|
|
@ -95,7 +95,7 @@ module Gitlab
|
|||
if c_line
|
||||
# If the line is still in D but also in C, it has turned from an
|
||||
# added line into an unchanged one.
|
||||
new_position = new_position(cd_diff, c_line, d_line)
|
||||
new_position = new_position(cd_diff, c_line, d_line, position.line_range)
|
||||
if valid_position?(new_position)
|
||||
# If the line is still in the MR, we don't treat this as outdated.
|
||||
{ position: new_position, outdated: false }
|
||||
|
@ -108,7 +108,7 @@ module Gitlab
|
|||
end
|
||||
else
|
||||
# If the line is still in D and not in C, it is still added.
|
||||
{ position: new_position(cd_diff, nil, d_line), outdated: false }
|
||||
{ position: new_position(cd_diff, nil, d_line, position.line_range), outdated: false }
|
||||
end
|
||||
else
|
||||
# If the line is no longer in D, it has been removed from the MR.
|
||||
|
@ -143,7 +143,7 @@ module Gitlab
|
|||
{ position: new_position(bd_diff, nil, d_line), outdated: true }
|
||||
else
|
||||
# If the line is still in C and not in D, it is still removed.
|
||||
{ position: new_position(cd_diff, c_line, nil), outdated: false }
|
||||
{ position: new_position(cd_diff, c_line, nil, position.line_range), outdated: false }
|
||||
end
|
||||
else
|
||||
# If the line is no longer in C, it has been removed outside of the MR.
|
||||
|
@ -174,7 +174,7 @@ module Gitlab
|
|||
|
||||
if c_line && d_line
|
||||
# If the line is still in C and D, it is still unchanged.
|
||||
new_position = new_position(cd_diff, c_line, d_line)
|
||||
new_position = new_position(cd_diff, c_line, d_line, position.line_range)
|
||||
if valid_position?(new_position)
|
||||
# If the line is still in the MR, we don't treat this as outdated.
|
||||
{ position: new_position, outdated: false }
|
||||
|
@ -188,7 +188,7 @@ module Gitlab
|
|||
# If the line is still in D but no longer in C, it has turned from
|
||||
# an unchanged line into an added one.
|
||||
# We don't treat this as outdated since the line is still in the MR.
|
||||
{ position: new_position(cd_diff, nil, d_line), outdated: false }
|
||||
{ position: new_position(cd_diff, nil, d_line, position.line_range), outdated: false }
|
||||
else # !d_line && (c_line || !c_line)
|
||||
# If the line is no longer in D, it has turned from an unchanged line
|
||||
# into a removed one.
|
||||
|
@ -196,12 +196,15 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def new_position(diff_file, old_line, new_line)
|
||||
Position.new(
|
||||
def new_position(diff_file, old_line, new_line, line_range = nil)
|
||||
params = {
|
||||
diff_file: diff_file,
|
||||
old_line: old_line,
|
||||
new_line: new_line
|
||||
)
|
||||
new_line: new_line,
|
||||
line_range: line_range
|
||||
}.compact
|
||||
|
||||
Position.new(**params)
|
||||
end
|
||||
|
||||
def valid_position?(position)
|
||||
|
|
|
@ -8,7 +8,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def migrate_to_remote_storage
|
||||
logger.info('Starting transfer to remote storage')
|
||||
logger.info('Starting transfer to object storage')
|
||||
|
||||
migrate(items_with_files_stored_locally, ObjectStorage::Store::REMOTE)
|
||||
end
|
||||
|
@ -38,11 +38,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def log_success(item, store)
|
||||
logger.info("Transferred #{item.class.name} ID #{item.id} of type #{item.file_type} with size #{item.size} to #{storage_label(store)} storage")
|
||||
logger.info("Transferred #{item.class.name} ID #{item.id} with size #{item.size} to #{storage_label(store)} storage")
|
||||
end
|
||||
|
||||
def log_error(err, item)
|
||||
logger.warn("Failed to transfer #{item.class.name} of type #{item.file_type} and ID #{item.id} with error: #{err.message}")
|
||||
logger.warn("Failed to transfer #{item.class.name} ID #{item.id} with error: #{err.message}")
|
||||
end
|
||||
|
||||
def storage_label(store)
|
||||
|
|
|
@ -8055,9 +8055,6 @@ msgstr ""
|
|||
msgid "ComplianceFrameworks|Add framework"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceFrameworks|All"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceFrameworks|Combines with the CI configuration at runtime."
|
||||
msgstr ""
|
||||
|
||||
|
@ -8094,9 +8091,6 @@ msgstr ""
|
|||
msgid "ComplianceFrameworks|Once a compliance framework is added it will appear here."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceFrameworks|Regulated"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceFrameworks|There are no compliance frameworks set up yet"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
|
|||
expect(active_project_access_tokens).to have_text('In')
|
||||
expect(active_project_access_tokens).to have_text('api')
|
||||
expect(active_project_access_tokens).to have_text('read_api')
|
||||
expect(active_project_access_tokens).to have_text('Maintainer')
|
||||
expect(created_project_access_token).not_to be_empty
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDropdown, GlDropdownDivider, GlFormInputGroup, GlButton } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
|
||||
import { tiptapExtension as Link } from '~/content_editor/extensions/link';
|
||||
|
@ -16,9 +16,6 @@ describe('content_editor/components/toolbar_link_button', () => {
|
|||
propsData: {
|
||||
tiptapEditor: editor,
|
||||
},
|
||||
stubs: {
|
||||
GlFormInputGroup,
|
||||
},
|
||||
});
|
||||
};
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
|
@ -45,9 +42,8 @@ describe('content_editor/components/toolbar_link_button', () => {
|
|||
});
|
||||
|
||||
describe('when there is an active link', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(editor, 'isActive');
|
||||
editor.isActive.mockReturnValueOnce(true);
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
|
@ -78,9 +74,35 @@ describe('content_editor/components/toolbar_link_button', () => {
|
|||
|
||||
expect(commands.focus).toHaveBeenCalled();
|
||||
expect(commands.unsetLink).toHaveBeenCalled();
|
||||
expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
|
||||
expect(commands.setLink).toHaveBeenCalledWith({
|
||||
href: 'https://example',
|
||||
'data-canonical-src': 'https://example',
|
||||
});
|
||||
expect(commands.run).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('on selection update', () => {
|
||||
it('updates link input box with canonical-src if present', async () => {
|
||||
jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
|
||||
'data-canonical-src': 'uploads/my-file.zip',
|
||||
href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
|
||||
});
|
||||
|
||||
await editor.emit('selectionUpdate', { editor });
|
||||
|
||||
expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip');
|
||||
});
|
||||
|
||||
it('updates link input box with link href otherwise', async () => {
|
||||
jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
|
||||
href: 'https://gitlab.com',
|
||||
});
|
||||
|
||||
await editor.emit('selectionUpdate', { editor });
|
||||
|
||||
expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is not an active link', () => {
|
||||
|
@ -106,7 +128,10 @@ describe('content_editor/components/toolbar_link_button', () => {
|
|||
await findApplyLinkButton().trigger('click');
|
||||
|
||||
expect(commands.focus).toHaveBeenCalled();
|
||||
expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
|
||||
expect(commands.setLink).toHaveBeenCalledWith({
|
||||
href: 'https://example',
|
||||
'data-canonical-src': 'https://example',
|
||||
});
|
||||
expect(commands.run).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import jsYaml from 'js-yaml';
|
||||
import { toArray } from 'lodash';
|
||||
import { getJSONFixture } from 'helpers/fixtures';
|
||||
|
||||
export const loadMarkdownApiResult = (testName) => {
|
||||
|
@ -15,5 +14,5 @@ export const loadMarkdownApiExamples = () => {
|
|||
const apiMarkdownYamlText = fs.readFileSync(apiMarkdownYamlPath);
|
||||
const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
|
||||
|
||||
return apiMarkdownExampleObjects.map((example) => toArray(example));
|
||||
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
|
||||
};
|
||||
|
|
|
@ -3,11 +3,15 @@ import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_proce
|
|||
|
||||
describe('markdown processing', () => {
|
||||
// Ensure we generate same markdown that was provided to Markdown API.
|
||||
it.each(loadMarkdownApiExamples())('correctly handles %s', async (testName, markdown) => {
|
||||
const { html } = loadMarkdownApiResult(testName);
|
||||
const contentEditor = createContentEditor({ renderMarkdown: () => html });
|
||||
await contentEditor.setSerializedContent(markdown);
|
||||
it.each(loadMarkdownApiExamples())(
|
||||
'correctly handles %s (context: %s)',
|
||||
async (name, context, markdown) => {
|
||||
const testName = context ? `${context}_${name}` : name;
|
||||
const { html, body } = loadMarkdownApiResult(testName);
|
||||
const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
|
||||
await contentEditor.setSerializedContent(markdown);
|
||||
|
||||
expect(contentEditor.getSerializedContent()).toBe(markdown);
|
||||
});
|
||||
expect(contentEditor.getSerializedContent()).toBe(markdown);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4,12 +4,32 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
|
||||
include ApiHelpers
|
||||
include WikiHelpers
|
||||
include JavaScriptFixturesHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:project) { create(:project, :public, :repository, group: group) }
|
||||
|
||||
let_it_be(:group_wiki) { create(:group_wiki, user: user) }
|
||||
let_it_be(:project_wiki) { create(:project_wiki, user: user) }
|
||||
|
||||
let(:group_wiki_page) { create(:wiki_page, wiki: group_wiki) }
|
||||
let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
|
||||
|
||||
fixture_subdir = 'api/markdown'
|
||||
|
||||
before(:all) do
|
||||
clean_frontend_fixtures(fixture_subdir)
|
||||
|
||||
group.add_owner(user)
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_group_wikis(true)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
markdown_examples = begin
|
||||
|
@ -19,14 +39,29 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
|
|||
end
|
||||
|
||||
markdown_examples.each do |markdown_example|
|
||||
context = markdown_example.fetch(:context, '')
|
||||
name = markdown_example.fetch(:name)
|
||||
|
||||
context "for #{name}" do
|
||||
context "for #{name}#{!context.empty? ? " (context: #{context})" : ''}" do
|
||||
let(:markdown) { markdown_example.fetch(:markdown) }
|
||||
|
||||
it "#{fixture_subdir}/#{name}.json" do
|
||||
post api("/markdown"), params: { text: markdown, gfm: true }
|
||||
name = "#{context}_#{name}" unless context.empty?
|
||||
|
||||
it "#{fixture_subdir}/#{name}.json" do
|
||||
api_url = case context
|
||||
when 'project'
|
||||
"/#{project.full_path}/preview_markdown"
|
||||
when 'group'
|
||||
"/groups/#{group.full_path}/preview_markdown"
|
||||
when 'project_wiki'
|
||||
"/#{project.full_path}/-/wikis/#{project_wiki_page.slug}/preview_markdown"
|
||||
when 'group_wiki'
|
||||
"/groups/#{group.full_path}/-/wikis/#{group_wiki_page.slug}/preview_markdown"
|
||||
else
|
||||
api "/markdown"
|
||||
end
|
||||
|
||||
post api_url, params: { text: markdown, gfm: true }
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,18 @@
|
|||
markdown: '---'
|
||||
- name: link
|
||||
markdown: '[GitLab](https://gitlab.com)'
|
||||
- name: attachment_link
|
||||
context: project_wiki
|
||||
markdown: '[test-file](test-file.zip)'
|
||||
- name: attachment_link
|
||||
context: project
|
||||
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
|
||||
- name: attachment_link
|
||||
context: group_wiki
|
||||
markdown: '[test-file](test-file.zip)'
|
||||
- name: attachment_link
|
||||
context: group
|
||||
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
|
||||
- name: code_block
|
||||
markdown: |-
|
||||
```javascript
|
||||
|
|
|
@ -114,7 +114,6 @@ describe('grafana integration component', () => {
|
|||
.then(() =>
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: `There was an error saving your changes. ${message}`,
|
||||
type: 'alert',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -184,9 +184,6 @@ describe('new file modal component', () => {
|
|||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'The name "test-path/test" is already taken in this directory.',
|
||||
type: 'alert',
|
||||
parent: expect.anything(),
|
||||
actionConfig: null,
|
||||
fadeTransition: false,
|
||||
addBodyClass: true,
|
||||
});
|
||||
|
|
|
@ -292,7 +292,7 @@ describe('IDE services', () => {
|
|||
|
||||
it('posts to usage endpoint', () => {
|
||||
const TEST_PROJECT_PATH = 'foo/bar';
|
||||
const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/usage_ping/web_ide_pipelines_count`;
|
||||
const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/service_ping/web_ide_pipelines_count`;
|
||||
|
||||
mock.onPost(axiosURL).reply(200);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as actions from '~/ide/stores/modules/clientside/actions';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
|
||||
const TEST_USAGE_URL = `${TEST_PROJECT_URL}/usage_ping/web_ide_clientside_preview`;
|
||||
const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/web_ide_clientside_preview`;
|
||||
|
||||
describe('IDE store module clientside actions', () => {
|
||||
let rootGetters;
|
||||
|
|
|
@ -39,7 +39,6 @@ describe('IncidentsSettingsService', () => {
|
|||
return service.updateSettings({}).then(() => {
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: expect.stringContaining(ERROR_MSG),
|
||||
type: 'alert',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
47
spec/frontend/milestones/milestone_utils_spec.js
Normal file
47
spec/frontend/milestones/milestone_utils_spec.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
|
||||
describe('sortMilestonesByDueDate', () => {
|
||||
useFakeDate(2021, 6, 22);
|
||||
const mockMilestones = [
|
||||
{
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
dueDate: '2021-01-01',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
dueDate: '2021-02-01',
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
dueDate: `2021-08-01`,
|
||||
},
|
||||
];
|
||||
|
||||
describe('sorts milestones', () => {
|
||||
it('expired milestones are kept at the bottom of the list', () => {
|
||||
const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
|
||||
|
||||
expect(sortedMilestones[2].id).toBe(mockMilestones[1].id); // milestone with id `1` is expired
|
||||
expect(sortedMilestones[3].id).toBe(mockMilestones[2].id); // milestone with id `4` is expired
|
||||
});
|
||||
|
||||
it('milestones with closest due date are kept at the top of the list', () => {
|
||||
const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
|
||||
|
||||
// milestone with id `3` & 2021-08-01 is closest to current date i.e. 2021-07-22
|
||||
expect(sortedMilestones[0].id).toBe(mockMilestones[3].id);
|
||||
});
|
||||
|
||||
it('milestones with no due date are kept between milestones with closest due date and expired milestones', () => {
|
||||
const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
|
||||
|
||||
// milestone with id `2` has no due date
|
||||
expect(sortedMilestones[1].id).toBe(mockMilestones[0].id);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -927,7 +927,6 @@ describe('Actions Notes Store', () => {
|
|||
expect(resp.hasFlash).toBe(true);
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'Your comment could not be submitted because something went wrong',
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
})
|
||||
|
@ -1011,7 +1010,6 @@ describe('Actions Notes Store', () => {
|
|||
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: TEST_ERROR_MESSAGE,
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
});
|
||||
|
@ -1030,7 +1028,6 @@ describe('Actions Notes Store', () => {
|
|||
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'Something went wrong while applying the suggestion. Please try again.',
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
});
|
||||
|
@ -1104,7 +1101,6 @@ describe('Actions Notes Store', () => {
|
|||
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: TEST_ERROR_MESSAGE,
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
});
|
||||
|
@ -1127,7 +1123,6 @@ describe('Actions Notes Store', () => {
|
|||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message:
|
||||
'Something went wrong while applying the batch of suggestions. Please try again.',
|
||||
type: 'alert',
|
||||
parent: flashContainer,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -205,7 +205,6 @@ describe('operation settings external dashboard component', () => {
|
|||
.then(() =>
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: `There was an error saving your changes. ${message}`,
|
||||
type: 'alert',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||
|
||||
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/milestones/milestone_utils');
|
||||
|
||||
const defaultStubs = {
|
||||
Portal: true,
|
||||
|
@ -112,6 +114,7 @@ describe('MilestoneToken', () => {
|
|||
|
||||
return waitForPromises().then(() => {
|
||||
expect(wrapper.vm.milestones).toEqual(mockMilestones);
|
||||
expect(sortMilestonesByDueDate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::GlobalIDType do
|
||||
include ::Gitlab::Graphql::Laziness
|
||||
include GraphqlHelpers
|
||||
include GlobalIDDeprecationHelpers
|
||||
|
||||
|
|
|
@ -288,6 +288,27 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
|
|||
new_line: old_position.new_line
|
||||
)
|
||||
end
|
||||
|
||||
context "when the position is multiline" do
|
||||
let(:old_position) do
|
||||
position(
|
||||
new_path: file_name,
|
||||
new_line: 2,
|
||||
line_range: {
|
||||
"start_line_code" => 1,
|
||||
"end_line_code" => 2
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "returns the new position along with line_range" do
|
||||
expect_new_position(
|
||||
new_path: old_position.new_path,
|
||||
new_line: old_position.new_line,
|
||||
line_range: old_position.line_range
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the file's content was changed between the old and the new diff" do
|
||||
|
@ -547,6 +568,29 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
|
|||
new_line: 2
|
||||
)
|
||||
end
|
||||
|
||||
context "when the position is multiline" do
|
||||
let(:old_position) do
|
||||
position(
|
||||
new_path: file_name,
|
||||
new_line: 2,
|
||||
line_range: {
|
||||
"start_line_code" => 1,
|
||||
"end_line_code" => 2
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "returns the new position but drops line_range information" do
|
||||
expect_change_position(
|
||||
old_path: file_name,
|
||||
new_path: file_name,
|
||||
old_line: nil,
|
||||
new_line: 2,
|
||||
line_range: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the file's content was changed between the old and the new diff" do
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples'
|
||||
|
||||
RSpec.describe Gitlab::LocalAndRemoteStorageMigration::ArtifactMigrater do
|
||||
before do
|
||||
stub_artifacts_object_storage(enabled: true)
|
||||
end
|
||||
|
||||
let!(:item) { create(:ci_job_artifact, :archive, file_store: start_store) }
|
||||
|
||||
it_behaves_like 'local and remote storage migration'
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples'
|
||||
|
||||
RSpec.describe Gitlab::LocalAndRemoteStorageMigration::PagesDeploymentMigrater do
|
||||
before do
|
||||
stub_pages_object_storage(::Pages::DeploymentUploader, enabled: true)
|
||||
end
|
||||
|
||||
let!(:item) { create(:pages_deployment, file_store: start_store) }
|
||||
|
||||
it_behaves_like 'local and remote storage migration'
|
||||
end
|
|
@ -103,19 +103,53 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
|
|||
end
|
||||
|
||||
describe '#append_data' do
|
||||
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: (+'😺').force_encoding('ASCII-8BIT')) }
|
||||
let(:initial_data) { (+'😺').force_encoding(Encoding::ASCII_8BIT) }
|
||||
let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: initial_data) }
|
||||
let(:data) { data_store.data(model) }
|
||||
|
||||
it 'appends ASCII data' do
|
||||
data_store.append_data(model, 'hello world', 4)
|
||||
context 'when ci_job_trace_force_encode is enabled' do
|
||||
it 'appends ASCII data' do
|
||||
data_store.append_data(model, +'hello world', 4)
|
||||
|
||||
expect(data.encoding).to eq(Encoding.find('ASCII-8BIT'))
|
||||
expect(data.force_encoding('UTF-8')).to eq('😺hello world')
|
||||
expect(data.encoding).to eq(Encoding::ASCII_8BIT)
|
||||
expect(data.force_encoding(Encoding::UTF_8)).to eq('😺hello world')
|
||||
end
|
||||
|
||||
it 'appends UTF-8 data' do
|
||||
data_store.append_data(model, +'Résumé', 4)
|
||||
|
||||
expect(data.encoding).to eq(Encoding::ASCII_8BIT)
|
||||
expect(data.force_encoding(Encoding::UTF_8)).to eq("😺Résumé")
|
||||
end
|
||||
|
||||
context 'when initial data is UTF-8' do
|
||||
let(:initial_data) { +'😺' }
|
||||
|
||||
it 'appends ASCII data' do
|
||||
data_store.append_data(model, +'hello world', 4)
|
||||
|
||||
expect(data.encoding).to eq(Encoding::ASCII_8BIT)
|
||||
expect(data.force_encoding(Encoding::UTF_8)).to eq('😺hello world')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'throws an exception when appending UTF-8 data' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_exception).and_call_original
|
||||
expect { data_store.append_data(model, 'Résumé', 4) }.to raise_exception(Encoding::CompatibilityError)
|
||||
context 'when ci_job_trace_force_encode is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_job_trace_force_encode: false)
|
||||
end
|
||||
|
||||
it 'appends ASCII data' do
|
||||
data_store.append_data(model, +'hello world', 4)
|
||||
|
||||
expect(data.encoding).to eq(Encoding::ASCII_8BIT)
|
||||
expect(data.force_encoding(Encoding::UTF_8)).to eq('😺hello world')
|
||||
end
|
||||
|
||||
it 'throws an exception when appending UTF-8 data' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_exception).and_call_original
|
||||
expect { data_store.append_data(model, +'Résumé', 4) }.to raise_exception(Encoding::CompatibilityError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline) }
|
||||
let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline) }
|
||||
let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline) }
|
||||
let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
||||
let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
||||
let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
||||
|
||||
let(:path) { %i[project pipeline] }
|
||||
let(:pipeline_graphql_data) { graphql_data_at(*path) }
|
||||
|
@ -79,16 +79,6 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_query_to_find_pipeline_shas(*pipelines)
|
||||
pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
|
||||
"pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
|
||||
end.join(' ')
|
||||
|
||||
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
|
||||
end
|
||||
|
||||
context 'when enough data is requested' do
|
||||
let(:fields) do
|
||||
query_graphql_field(:jobs, nil,
|
||||
|
@ -282,4 +272,69 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'N+1 queries on stages jobs' do
|
||||
let(:depth) { 5 }
|
||||
let(:fields) do
|
||||
<<~FIELDS
|
||||
stages {
|
||||
nodes {
|
||||
name
|
||||
groups {
|
||||
nodes {
|
||||
name
|
||||
jobs {
|
||||
nodes {
|
||||
name
|
||||
needs {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
status: detailedStatus {
|
||||
tooltip
|
||||
hasDetails
|
||||
detailsPath
|
||||
action {
|
||||
buttonTitle
|
||||
path
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
|
||||
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
|
||||
# warm up
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
create(:ci_build, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
||||
create(:ci_build, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
||||
|
||||
expect do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end.not_to exceed_all_query_limit(control)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_query_to_find_pipeline_shas(*pipelines)
|
||||
pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
|
||||
"pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
|
||||
end.join(' ')
|
||||
|
||||
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,6 +38,7 @@ RSpec.describe API::ResourceAccessTokens do
|
|||
|
||||
expect(api_get_token["name"]).to eq(token.name)
|
||||
expect(api_get_token["scopes"]).to eq(token.scopes)
|
||||
expect(api_get_token["access_level"]).to eq(project.team.max_member_access(token.user.id))
|
||||
expect(api_get_token["expires_at"]).to eq(token.expires_at.to_date.iso8601)
|
||||
expect(api_get_token).not_to have_key('token')
|
||||
end
|
||||
|
|
|
@ -418,19 +418,6 @@ RSpec.describe WebHookService do
|
|||
described_class.new(other_hook, data, :push_hooks).async_execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(web_hooks_rate_limit: false)
|
||||
end
|
||||
|
||||
it 'queues a worker without tracking the call' do
|
||||
expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
|
||||
expect_to_perform_worker(project_hook)
|
||||
|
||||
service_instance.async_execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook has custom context attributes' do
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'local and remote storage migration' do
|
||||
let(:logger) { Logger.new("/dev/null") }
|
||||
let(:migrater) { described_class.new(logger) }
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:start_store, :end_store, :method) do
|
||||
ObjectStorage::Store::LOCAL | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage
|
||||
ObjectStorage::Store::REMOTE | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
||||
ObjectStorage::Store::REMOTE | ObjectStorage::Store::LOCAL | :migrate_to_local_storage
|
||||
ObjectStorage::Store::LOCAL | ObjectStorage::Store::LOCAL | :migrate_to_local_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:storage_name) { end_store == ObjectStorage::Store::REMOTE ? 'object' : 'local' }
|
||||
|
||||
it 'successfully migrates' do
|
||||
expect(logger).to receive(:info).with("Starting transfer to #{storage_name} storage")
|
||||
|
||||
if start_store != end_store
|
||||
expect(logger).to receive(:info).with("Transferred #{item.class.name} ID #{item.id} with size #{item.size} to #{storage_name} storage")
|
||||
end
|
||||
|
||||
expect(item.file_store).to eq(start_store)
|
||||
|
||||
migrater.send(method)
|
||||
|
||||
expect(item.reload.file_store).to eq(end_store)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when migration fails' do
|
||||
let(:start_store) { ObjectStorage::Store::LOCAL }
|
||||
|
||||
it 'prints error' do
|
||||
expect_next_instance_of(item.file.class) do |file|
|
||||
expect(file).to receive(:migrate!).and_raise("error message")
|
||||
end
|
||||
|
||||
expect(logger).to receive(:info).with("Starting transfer to object storage")
|
||||
|
||||
expect(logger).to receive(:warn).with("Failed to transfer #{item.class.name} ID #{item.id} with error: error message")
|
||||
|
||||
expect(item.file_store).to eq(start_store)
|
||||
|
||||
migrater.migrate_to_remote_storage
|
||||
|
||||
expect(item.reload.file_store).to eq(start_store)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue