Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-24 12:08:07 +00:00
parent 23b60ed2c1
commit b4e7d9d839
77 changed files with 590 additions and 237 deletions

View file

@ -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)

View file

@ -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 });
},

View file

@ -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)}` : ''})`;
},
};

View file

@ -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);
};

View file

@ -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'),
},

View file

@ -40,6 +40,5 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
type: 'alert',
});
};

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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) {

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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);
};

View file

@ -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,
});

View file

@ -24,7 +24,6 @@ export default class IncidentsSettingsService {
createFlash({
message: `${ERROR_MSG} ${message}`,
type: 'alert',
});
});
}

View file

@ -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 = [];

View 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;
}

View file

@ -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),
});
}

View file

@ -222,7 +222,6 @@ export default {
);
createFlash({
message: msg,
type: 'alert',
parent: this.$el,
});
this.$refs.noteForm.note = noteText;

View file

@ -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);

View file

@ -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,
});
});

View file

@ -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,
});
})

View file

@ -37,6 +37,5 @@ export const receiveSaveChangesError = (_, error) => {
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
type: 'alert',
});
};

View file

@ -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;

View file

@ -24,7 +24,6 @@ export default class TaskList {
return createFlash({
message: errorMessages || __('Update failed'),
type: 'alert',
});
};

View file

@ -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>

View file

@ -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(() => {

View file

@ -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,
});
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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?(

View file

@ -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) },

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
f8b8276ed7e120b61f6748a328590a98f0e444e0d26bcb1a2b0daa54c3643acd

View file

@ -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;

View file

@ -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.

View 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. |

View file

@ -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
}
```

View 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

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class ResourceAccessTokenWithToken < Entities::ResourceAccessToken
expose :token
end
end
end

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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 ""

View file

@ -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

View file

@ -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();
});
});

View file

@ -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]);
};

View file

@ -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);
},
);
});

View file

@ -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

View file

@ -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

View file

@ -114,7 +114,6 @@ describe('grafana integration component', () => {
.then(() =>
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
type: 'alert',
}),
);
});

View file

@ -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,
});

View file

@ -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);

View file

@ -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;

View file

@ -39,7 +39,6 @@ describe('IncidentsSettingsService', () => {
return service.updateSettings({}).then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringContaining(ERROR_MSG),
type: 'alert',
});
});
});

View 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);
});
});
});

View file

@ -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,
});
});

View file

@ -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',
}),
);
});

View file

@ -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();
});
});

View file

@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Types::GlobalIDType do
include ::Gitlab::Graphql::Laziness
include GraphqlHelpers
include GlobalIDDeprecationHelpers

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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