Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-21 15:09:33 +00:00
parent cd6e1ccea4
commit c39912f553
41 changed files with 952 additions and 916 deletions

View File

@ -1,154 +1,23 @@
<script>
import {
GlButton,
GlModalDirective,
GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
CREATE_TOKEN_MODAL,
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
} from '../constants';
import createNewAgentToken from '../graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import { addAgentTokenToStore } from '../graphql/cache_update';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
import { GlButton, GlModalDirective, GlTooltip } from '@gitlab/ui';
import { s__ } from '~/locale';
import { CREATE_TOKEN_MODAL } from '../constants';
export default {
components: {
AgentToken,
GlButton,
GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
},
directives: {
GlModalDirective,
},
mixins: [trackingMixin],
inject: ['agentName', 'projectPath', 'canAdminCluster'],
props: {
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
inject: ['canAdminCluster'],
modalId: CREATE_TOKEN_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
TOKEN_NAME_LIMIT,
i18n: {
createTokenButton: s__('ClusterAgents|Create token'),
modalTitle: s__('ClusterAgents|Create agent access token'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
errorTitle: s__('ClusterAgents|Failed to create a token'),
dropdownDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
),
modalCancel: __('Cancel'),
modalClose: __('Close'),
tokenNameLabel: __('Name'),
tokenDescriptionLabel: __('Description (optional)'),
},
data() {
return {
token: {
name: null,
description: null,
},
agentToken: null,
error: null,
loading: false,
variables: {
agentName: this.agentName,
projectPath: this.projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...this.cursor,
},
};
},
computed: {
modalBtnDisabled() {
return this.loading || !this.hasTokenName;
},
hasTokenName() {
return Boolean(this.token.name?.length);
},
},
methods: {
async createToken() {
this.loading = true;
this.error = null;
try {
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation();
if (tokenErrors?.length > 0) {
throw new Error(tokenErrors[0]);
}
this.agentToken = secret;
} catch (error) {
if (error) {
this.error = error.message;
} else {
this.error = this.$options.i18n.unknownError;
}
} finally {
this.loading = false;
}
},
resetModal() {
this.agentToken = null;
this.token.name = null;
this.token.description = null;
this.error = null;
},
closeModal() {
this.$refs.modal.hide();
},
createAgentTokenMutation() {
return this.$apollo
.mutate({
mutation: createNewAgentToken,
variables: {
input: {
clusterAgentId: this.clusterAgentId,
name: this.token.name,
description: this.token.description,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentTokenToStore(
store,
clusterAgentTokenCreate,
getClusterAgentQuery,
this.variables,
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
},
};
</script>
@ -170,82 +39,5 @@ export default {
:title="$options.i18n.dropdownDisabledHint"
/>
</div>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN)"
>
<gl-alert
v-if="error"
:title="$options.i18n.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-5"
>
{{ error }}
</gl-alert>
<template v-if="!agentToken">
<gl-form-group :label="$options.i18n.tokenNameLabel">
<gl-form-input
v-model="token.name"
:max-length="$options.TOKEN_NAME_LIMIT"
:disabled="loading"
required
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.tokenDescriptionLabel">
<gl-form-textarea v-model="token.description" :disabled="loading" name="description" />
</gl-form-group>
</template>
<agent-token
v-else
:agent-name="agentName"
:agent-token="agentToken"
:modal-id="$options.modalId"
/>
<template #modal-footer>
<gl-button
v-if="!agentToken && !loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
data-testid="agent-token-close-button"
@click="closeModal"
>{{ $options.i18n.modalCancel }}
</gl-button>
<gl-button
v-if="!agentToken"
:disabled="modalBtnDisabled"
:loading="loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="create-token"
variant="confirm"
type="submit"
@click="createToken"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-button
v-else
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
variant="confirm"
@click="closeModal"
>{{ $options.i18n.modalClose }}
</gl-button>
</template>
</gl-modal>
</div>
</template>

View File

@ -0,0 +1,218 @@
<script>
import { GlButton, GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
CREATE_TOKEN_MODAL,
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
} from '../constants';
import createNewAgentToken from '../graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import { addAgentTokenToStore } from '../graphql/cache_update';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default {
components: {
AgentToken,
GlButton,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
},
mixins: [trackingMixin],
inject: ['agentName', 'projectPath'],
props: {
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
modalId: CREATE_TOKEN_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
TOKEN_NAME_LIMIT,
i18n: {
createTokenButton: s__('ClusterAgents|Create token'),
modalTitle: s__('ClusterAgents|Create agent access token'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
errorTitle: s__('ClusterAgents|Failed to create a token'),
modalCancel: __('Cancel'),
modalClose: __('Close'),
tokenNameLabel: __('Name'),
tokenDescriptionLabel: __('Description (optional)'),
},
data() {
return {
token: {
name: null,
description: null,
},
agentToken: null,
error: null,
loading: false,
variables: {
agentName: this.agentName,
projectPath: this.projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...this.cursor,
},
};
},
computed: {
modalBtnDisabled() {
return this.loading || !this.hasTokenName;
},
hasTokenName() {
return Boolean(this.token.name?.length);
},
},
methods: {
async createToken() {
this.loading = true;
this.error = null;
try {
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation();
if (tokenErrors?.length > 0) {
throw new Error(tokenErrors[0]);
}
this.agentToken = secret;
} catch (error) {
this.error = error ? error.message : this.$options.i18n.unknownError;
} finally {
this.loading = false;
}
},
resetModal() {
this.agentToken = null;
this.token.name = null;
this.token.description = null;
this.error = null;
},
closeModal() {
this.$refs.modal.hide();
},
createAgentTokenMutation() {
return this.$apollo
.mutate({
mutation: createNewAgentToken,
variables: {
input: {
clusterAgentId: this.clusterAgentId,
name: this.token.name,
description: this.token.description,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentTokenToStore(
store,
clusterAgentTokenCreate,
getClusterAgentQuery,
this.variables,
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN)"
>
<gl-alert
v-if="error"
:title="$options.i18n.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-5"
>
{{ error }}
</gl-alert>
<template v-if="!agentToken">
<gl-form-group :label="$options.i18n.tokenNameLabel" label-for="token-name">
<gl-form-input
id="token-name"
v-model="token.name"
:max-length="$options.TOKEN_NAME_LIMIT"
:disabled="loading"
required
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.tokenDescriptionLabel" label-for="token-description">
<gl-form-textarea
id="token-description"
v-model="token.description"
:disabled="loading"
name="description"
/>
</gl-form-group>
</template>
<agent-token
v-else
:agent-name="agentName"
:agent-token="agentToken"
:modal-id="$options.modalId"
/>
<template #modal-footer>
<gl-button
v-if="!agentToken && !loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
data-testid="agent-token-close-button"
@click="closeModal"
>{{ $options.i18n.modalCancel }}
</gl-button>
<gl-button
v-if="!agentToken"
:disabled="modalBtnDisabled"
:loading="loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="create-token"
variant="confirm"
type="submit"
@click="createToken"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-button
v-else
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
variant="confirm"
@click="closeModal"
>{{ $options.i18n.modalClose }}
</gl-button>
</template>
</gl-modal>
</template>

View File

@ -148,7 +148,7 @@ export default {
},
hideModal() {
this.resetModal();
this.$refs.modal.hide();
this.$refs.modal?.hide();
},
},
};

View File

@ -3,6 +3,7 @@ import { GlEmptyState, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CreateTokenButton from './create_token_button.vue';
import CreateTokenModal from './create_token_modal.vue';
import RevokeTokenButton from './revoke_token_button.vue';
export default {
@ -13,6 +14,7 @@ export default {
GlTruncate,
TimeAgoTooltip,
CreateTokenButton,
CreateTokenModal,
RevokeTokenButton,
},
i18n: {
@ -85,57 +87,57 @@ export default {
</script>
<template>
<div v-if="tokens.length">
<create-token-button
class="gl-text-right gl-my-5"
:cluster-agent-id="clusterAgentId"
:cursor="cursor"
/>
<div>
<div v-if="tokens.length">
<create-token-button class="gl-text-right gl-my-5" />
<gl-table
:items="tokens"
:fields="fields"
fixed
stacked="md"
head-variant="white"
thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
>
<template #cell(lastUsed)="{ item }">
<time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" />
<span v-else>{{ $options.i18n.neverUsed }}</span>
<gl-table
:items="tokens"
:fields="fields"
fixed
stacked="md"
head-variant="white"
thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
>
<template #cell(lastUsed)="{ item }">
<time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" />
<span v-else>{{ $options.i18n.neverUsed }}</span>
</template>
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #cell(createdBy)="{ item }">
<span>{{ createdByName(item) }}</span>
</template>
<template #cell(description)="{ item }">
<div v-if="item.description" :id="`tooltip-description-container-${item.id}`">
<gl-truncate :id="`tooltip-description-${item.id}`" :text="item.description" />
<gl-tooltip
:container="`tooltip-description-container-${item.id}`"
:target="`tooltip-description-${item.id}`"
placement="top"
>
{{ item.description }}
</gl-tooltip>
</div>
</template>
<template #cell(actions)="{ item }">
<revoke-token-button :token="item" :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-table>
</div>
<gl-empty-state v-else :title="$options.i18n.noTokens">
<template #actions>
<create-token-button />
</template>
</gl-empty-state>
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #cell(createdBy)="{ item }">
<span>{{ createdByName(item) }}</span>
</template>
<template #cell(description)="{ item }">
<div v-if="item.description" :id="`tooltip-description-container-${item.id}`">
<gl-truncate :id="`tooltip-description-${item.id}`" :text="item.description" />
<gl-tooltip
:container="`tooltip-description-container-${item.id}`"
:target="`tooltip-description-${item.id}`"
placement="top"
>
{{ item.description }}
</gl-tooltip>
</div>
</template>
<template #cell(actions)="{ item }">
<revoke-token-button :token="item" :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-table>
<create-token-modal :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</div>
<gl-empty-state v-else :title="$options.i18n.noTokens">
<template #actions>
<create-token-button :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-empty-state>
</template>

View File

@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
VERSION = '0.41.0'
VERSION = '0.42.0'
self.table_name = 'clusters_applications_runners'

View File

@ -108,13 +108,9 @@ class Deployment < ApplicationRecord
end
end
after_transition any => :running do |deployment|
after_transition any => :running do |deployment, transition|
deployment.run_after_commit do
if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
deployment.execute_hooks(Time.current)
else
Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
end
end
@ -126,13 +122,9 @@ class Deployment < ApplicationRecord
end
end
after_transition any => FINISHED_STATUSES do |deployment|
after_transition any => FINISHED_STATUSES do |deployment, transition|
deployment.run_after_commit do
if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
deployment.execute_hooks(Time.current)
else
Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
end
end
@ -269,8 +261,8 @@ class Deployment < ApplicationRecord
Commit.truncate_sha(sha)
end
def execute_hooks(status_changed_at)
deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at)
def execute_hooks(status, status_changed_at)
deployment_data = Gitlab::DataBuilder::Deployment.build(self, status, status_changed_at)
project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_integrations(deployment_data, :deployment_hooks)
end

View File

@ -63,7 +63,7 @@ module Integrations
return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present?
Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current)
end
def releases_events_data

View File

@ -11,5 +11,6 @@
= form_tag search_path, method: :get, class: 'form-inline-flex' do |f|
.field
= search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control'
= button_tag _('Search'), class: 'gl-button btn btn-sm btn-success', name: nil, type: 'submit'
= render Pajamas::ButtonComponent.new(variant: :confirm, size: :small, type: :submit) do
= _('Search')
= render 'errors/footer'

View File

@ -45,7 +45,8 @@
&middot;
%span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) }
- if member.expires?
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) }
- preposition = current_user.time_display_relative ? '' : 'on'
= _("Expires %{preposition} %{expires_at}").html_safe % { expires_at: time_ago_with_tooltip(member.expires_at), preposition: preposition }
- else
= image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''

View File

@ -16,7 +16,7 @@ module Deployments
log_extra_metadata_on_done(:deployment_project_id, deploy.project.id)
log_extra_metadata_on_done(:deployment_id, params[:deployment_id])
deploy.execute_hooks(params[:status_changed_at].to_time)
deploy.execute_hooks(params[:status], params[:status_changed_at].to_time)
end
end
end

View File

@ -1,8 +0,0 @@
---
name: deployment_hooks_skip_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83351
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356468
milestone: '14.10'
type: development
group: group::integrations
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: gitaly_revlist_for_repo_size
introduced_by_url:
rollout_issue_url:
milestone:
type: undefined
group:
default_enabled: false

View File

@ -19669,8 +19669,9 @@ The status of the security scan.
| Value | Description |
| ----- | ----------- |
| <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project only. |
| <a id="securitypolicyrelationtypeinherited"></a>`INHERITED` | Policies defined for the project and project's ancestor groups. |
| <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project/group only. |
| <a id="securitypolicyrelationtypeinherited"></a>`INHERITED` | Policies defined for the project/group and ancestor groups. |
| <a id="securitypolicyrelationtypeinherited_only"></a>`INHERITED_ONLY` | Policies defined for the project/group's ancestor groups only. |
### `SecurityReportTypeEnum`

View File

@ -79,12 +79,10 @@ page, with these behaviors:
- **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:`
- **At capacity** - 🔴 `:red_circle:`
- **Focus mode** - 💡 `:bulb:` (focusing on their team's work)
1. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
are three times as likely to be picked as other reviewers.
1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers.
- Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers.
- Trainee maintainers with 🔵 `:large_blue_circle:` are four times as likely to be picked as other reviewers.
- [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers.
1. People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked.
1. It always picks the same reviewers and maintainers for the same

View File

@ -668,6 +668,19 @@ The most popular public email domains cannot be restricted, such as:
- `hotmail.com`, `hotmail.co.uk`, `hotmail.fr`
- `msn.com`, `live.com`, `outlook.com`
## Restrict Git access protocols
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/365601) in GitLab 15.1.
Access to the group's repositories via SSH or HTTP(S) can be restricted to individual protocols. This setting is overridden by the instance setting configured in the GitLab Admin.
To alter the permitted Git access protocols:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions and group features** section.
1. Choose the allowed protocols from **Enable Git access protocols**
1. Select **Save changes**
## Group file templates **(PREMIUM)**
Use group file templates to share a set of templates for common file

View File

@ -5,7 +5,8 @@ module Gitlab
module Deployment
extend self
def build(deployment, status_changed_at)
# NOTE: Time-sensitive attributes should be explicitly passed as argument instead of reading from database.
def build(deployment, status, status_changed_at)
# Deployments will not have a deployable when created using the API.
deployable_url =
if deployment.deployable
@ -22,9 +23,13 @@ module Gitlab
Gitlab::UrlBuilder.build(deployment.deployed_by)
end
# `status` argument could be `nil` during the upgrade. We can remove `deployment.status` in GitLab 15.5.
# See https://docs.gitlab.com/ee/development/multi_version_compatibility.html for more info.
deployment_status = status || deployment.status
{
object_kind: 'deployment',
status: deployment.status,
status: deployment_status,
status_changed_at: status_changed_at,
deployment_id: deployment.id,
deployable_id: deployment.deployable_id,

View File

@ -15315,7 +15315,7 @@ msgstr ""
msgid "Expires"
msgstr ""
msgid "Expires in %{expires_at}"
msgid "Expires %{preposition} %{expires_at}"
msgstr ""
msgid "Expires on"

View File

@ -218,6 +218,31 @@ module QA
run_git('git --no-pager branch --list --remotes --format="%(refname:lstrip=3)"').to_s.split("\n")
end
# Gets the size of the repository using `git rev-list --all --objects --use-bitmap-index --disk-usage` as
# Gitaly does (see https://gitlab.com/gitlab-org/gitlab/-/issues/357680)
def local_size
internal_refs = %w[
refs/keep-around/
refs/merge-requests/
refs/pipelines/
refs/remotes/
refs/tmp/
refs/environments/
]
cmd = <<~CMD
git rev-list #{internal_refs.map { |r| "--exclude='#{r}*'" }.join(' ')} \
--not --alternate-refs --not \
--all --objects --use-bitmap-index --disk-usage
CMD
run_git(cmd).to_i
end
# Performs garbage collection
def run_gc
run_git('git gc')
end
private
attr_reader :uri, :username, :password, :ssh, :use_lfs

View File

@ -224,6 +224,10 @@ module QA
"#{api_get_path}/releases"
end
def api_housekeeping_path
"/projects/#{id}/housekeeping"
end
def api_post_body
post_body = {
name: name,
@ -447,6 +451,31 @@ module QA
end
end
# Calls the API endpoint that triggers the backend service that performs repository housekeeping (garbage
# collection and similar tasks).
def perform_housekeeping
Runtime::Logger.debug("Calling API endpoint #{api_housekeeping_path}")
response = post(request_url(api_housekeeping_path), nil)
unless response.code == HTTP_STATUS_CREATED
raise ResourceQueryError,
"Could not perform housekeeping. Request returned (#{response.code}): `#{response.body}`."
end
end
# Gets project statistics.
#
# @return [Hash] the project usage data including repository size.
def statistics
response = get(request_url("#{api_get_path}?statistics=true"))
data = parse_body(response)
raise "Could not get project usage statistics" unless data.key?(:statistics)
data[:statistics]
end
protected
# Return subset of fields for comparing projects

View File

@ -10,7 +10,7 @@ module QA
let(:differ) { RSpec::Support::Differ.new(color: true) }
let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' }
let(:gitlab_project) { ENV['QA_LARGE_IMPORT_REPO'] || 'dri' }
let(:gitlab_source_address) { 'https://staging.gitlab.com' }
let(:gitlab_source_address) { ENV['QA_LARGE_IMPORT_SOURCE_URL'] || 'https://staging.gitlab.com' }
let(:import_wait_duration) do
{

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Repository Usage Quota', :skip_live_env, feature_flag: {
name: 'gitaly_revlist_for_repo_size',
scope: :global
} do
let(:project_name) { "repository-usage-#{SecureRandom.hex(8)}" }
let!(:flag_enabled) { Runtime::Feature.enabled?(:gitaly_revlist_for_repo_size) }
before do
Runtime::Feature.enable(:gitaly_revlist_for_repo_size)
end
after do
Runtime::Feature.set({ gitaly_revlist_for_repo_size: flag_enabled })
end
# Previously, GitLab could report a size many times larger than a cloned copy. For example, 37Gb reported for a
# repo that is 2Gb when cloned.
#
# After changing Gitaly to use `git rev-list` to determine the size of a repo, the reported size is much more
# accurate. Nonetheless, the size of a clone is still not necessarily the same as the original. We can't do a
# precise comparison because of the non-deterministic nature of how git packs files. Depending on the history of
# the repository the sizes can vary considerably. For example, at the time of writing this a clone of
# www-gitlab-com was 5.27Gb, about 5% smaller than the size GitLab reported, 5.51Gb.
#
# There are unit tests to verify the accuracy of GitLab's determination of repo size, so for this test we
# attempt to detect large differences that could indicate a regression to previous behavior.
it 'matches cloned repo usage to reported usage',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/365196' do
project = Resource::Project.fabricate_via_api! do |project|
project.name = project_name
end
shared_data = SecureRandom.random_bytes(500000)
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = 'data.dat'
push.file_content = SecureRandom.random_bytes(500000) + shared_data
push.commit_message = 'Add file'
end
local_size = Git::Repository.perform do |repository|
repository.uri = project.repository_http_location.uri
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
# These two commits add a total of 1mb, but half of that is the same as content that has already been added to
# the repository, so garbage collection will deduplicate it.
repository.commit_file("new-data", SecureRandom.random_bytes(500000), "Add file")
repository.commit_file("redudant-data", shared_data, "Add file")
repository.run_gc
repository.push_changes
repository.local_size
end
# The size of the remote repository after all content has been added.
initial_size = project.statistics[:repository_size].to_i
# This is an async process and as a user we have no way to know when it's complete unless the statistics are
# updated
Support::Retrier.retry_until(max_duration: 60, sleep_interval: 5) do
# This should perform the same deduplication as in the local repo
project.perform_housekeeping
project.statistics[:repository_size].to_i != initial_size
end
twentyfive_percent = local_size.to_i * 0.25
expect(project.statistics[:repository_size].to_i).to be_within(twentyfive_percent).of(local_size)
end
end
end
end

View File

@ -15,6 +15,10 @@ module QA
def success?
exitstatus == 0 && !response.include?('Error encountered')
end
def to_i
response.to_i
end
end
def run(command_str, env: [], max_attempts: 1, log_prefix: '')

View File

@ -231,7 +231,7 @@ module Glfm
name = example.fetch(:name)
json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json')
existing_hash.dig(name)
existing_hash[name]
else
wysiwyg_html_and_json_hash.dig(name, 'json')
end

View File

@ -12,7 +12,7 @@ RSpec.describe 'Admin Groups' do
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:current_user) { create(:admin) }
let_it_be_with_reload(:current_user) { create(:admin) }
before do
sign_in(current_user)
@ -231,6 +231,28 @@ RSpec.describe 'Admin Groups' do
it_behaves_like 'adds user into a group' do
let(:user_selector) { user.email }
end
context 'when membership is set to expire' do
it 'renders relative time' do
expire_time = Time.current + 2.days
current_user.update!(time_display_relative: true)
group.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_group_path(group)
expect(page).to have_content(/Expires in \d day/)
end
it 'renders absolute time' do
expire_time = Time.current.tomorrow.middle_of_day
current_user.update!(time_display_relative: false)
group.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_group_path(group)
expect(page).to have_content("Expires on #{expire_time.strftime('%b %-d')}")
end
end
end
describe 'add admin himself to a group' do

View File

@ -7,15 +7,37 @@ RSpec.describe "Admin::Projects" do
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
let(:user) { create :user }
let(:project) { create(:project, :with_namespace_settings) }
let(:current_user) { create(:admin) }
let_it_be_with_reload(:user) { create :user }
let_it_be_with_reload(:project) { create(:project, :with_namespace_settings) }
let_it_be_with_reload(:current_user) { create(:admin) }
before do
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end
describe 'when membership is set to expire', :js do
it 'renders relative time' do
expire_time = Time.current + 2.days
current_user.update!(time_display_relative: true)
project.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_project_path(project)
expect(page).to have_content(/Expires in \d day/)
end
it 'renders absolute time' do
expire_time = Time.current.tomorrow.middle_of_day
current_user.update!(time_display_relative: false)
project.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_project_path(project)
expect(page).to have_content("Expires on #{expire_time.strftime('%b %-d')}")
end
end
describe "GET /admin/projects" do
let!(:archived_project) { create :project, :public, :archived }

View File

@ -1,262 +1,71 @@
import { GlButton, GlTooltip, GlModal, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlButton, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import {
clusterAgentToken,
getTokenResponse,
createAgentTokenErrorResponse,
} from '../../mock_data';
Vue.use(VueApollo);
import { CREATE_TOKEN_MODAL } from '~/clusters/agents/constants';
describe('CreateTokenButton', () => {
let wrapper;
let apolloProvider;
let trackingSpy;
let createResponse;
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
const defaultProvide = {
agentName,
projectPath,
canAdminCluster: true,
};
const propsData = {
clusterAgentId,
cursor,
};
const findModal = () => wrapper.findComponent(GlModal);
const findBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findAlert = () => wrapper.findComponent(GlAlert);
const findButton = () => wrapper.findComponent(GlButton);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
expect(element.attributes('disabled')).toBe('true');
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
};
const createMockApolloProvider = ({ mutationResponse }) => {
createResponse = jest.fn().mockResolvedValue(mutationResponse);
return createMockApollo([[createNewAgentToken, createResponse]]);
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getClusterAgentQuery,
data: getTokenResponse.data,
variables: {
agentName,
projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
};
const createWrapper = async ({ provideData = {} } = {}) => {
const createWrapper = ({ provideData = {} } = {}) => {
wrapper = shallowMountExtended(CreateTokenButton, {
apolloProvider,
provide: {
...defaultProvide,
...provideData,
},
propsData,
directives: {
GlModalDirective: createMockDirective(),
},
stubs: {
GlModal,
GlTooltip,
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const mockCreatedResponse = (mutationResponse) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
writeQuery();
createWrapper();
findInput().vm.$emit('input', 'new-token');
findTextarea().vm.$emit('input', 'new-token-description');
findActionButton().vm.$emit('click');
return waitForPromises();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
describe('create agent token action', () => {
describe('when user can create token', () => {
beforeEach(() => {
createWrapper();
});
it('displays create agent token button', () => {
expect(findBtn().text()).toBe('Create token');
expect(findButton().text()).toBe('Create token');
});
describe('when user cannot create token', () => {
beforeEach(() => {
createWrapper({ provideData: { canAdminCluster: false } });
});
it('disabled the button', () => {
expect(findBtn().attributes('disabled')).toBe('true');
});
it('shows a disabled tooltip', () => {
expect(findTooltip().attributes('title')).toBe(
'Requires a Maintainer or greater role to perform these actions',
);
});
it('displays create agent token button as not disabled', () => {
expect(findButton().attributes('disabled')).toBeUndefined();
});
describe('when user can create a token and clicks the button', () => {
beforeEach(() => {
findBtn().vm.$emit('click');
});
it('triggers the modal', () => {
const binding = getBinding(findButton().element, 'gl-modal-directive');
it('displays a token creation modal', () => {
expect(findModal().isVisible()).toBe(true);
});
expect(binding.value).toBe(CREATE_TOKEN_MODAL);
});
});
describe('initial state', () => {
it('renders an input for the token name', () => {
expect(findInput().exists()).toBe(true);
expectDisabledAttribute(findInput(), false);
expect(findInput().attributes('max-length')).toBe(TOKEN_NAME_LIMIT.toString());
});
describe('when user cannot create token', () => {
beforeEach(() => {
createWrapper({ provideData: { canAdminCluster: false } });
});
it('renders a textarea for the token description', () => {
expect(findTextarea().exists()).toBe(true);
expectDisabledAttribute(findTextarea(), false);
});
it('disabled the button', () => {
expect(findButton().attributes('disabled')).toBe('true');
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
});
it('sends tracking event for modal shown', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
});
});
});
describe('when user inputs the token name', () => {
beforeEach(() => {
expectDisabledAttribute(findActionButton(), true);
findInput().vm.$emit('input', 'new-token');
});
it('enables the next button', () => {
expectDisabledAttribute(findActionButton(), false);
});
});
describe('when user clicks the create-token button', () => {
beforeEach(async () => {
const loadingResponse = new Promise(() => {});
await mockCreatedResponse(loadingResponse);
findInput().vm.$emit('input', 'new-token');
findActionButton().vm.$emit('click');
});
it('disables the create-token button', () => {
expectDisabledAttribute(findActionButton(), true);
});
it('hides the cancel button', () => {
expect(findCancelButton().exists()).toBe(false);
});
});
describe('creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(clusterAgentToken);
});
it('creates a token', () => {
expect(createResponse).toHaveBeenCalledWith({
input: { clusterAgentId, name: 'new-token', description: 'new-token-description' },
});
});
it('shows agent instructions', () => {
expect(findAgentInstructions().props()).toMatchObject({
agentName,
agentToken: 'token-secret',
modalId: CREATE_TOKEN_MODAL,
});
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe('Close');
expectDisabledAttribute(findActionButton(), false);
});
});
describe('error creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
it('shows a disabled tooltip', () => {
expect(findTooltip().attributes('title')).toBe(
'Requires a Maintainer or greater role to perform these actions',
);
});
});
});

View File

@ -0,0 +1,223 @@
import { GlButton, GlModal, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import CreateTokenModal from '~/clusters/agents/components/create_token_modal.vue';
import {
clusterAgentToken,
getTokenResponse,
createAgentTokenErrorResponse,
} from '../../mock_data';
Vue.use(VueApollo);
describe('CreateTokenModal', () => {
let wrapper;
let apolloProvider;
let trackingSpy;
let createResponse;
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
const provide = {
agentName,
projectPath,
};
const propsData = {
clusterAgentId,
cursor,
};
const findModal = () => wrapper.findComponent(GlModal);
const findInput = () => wrapper.findComponent(GlFormInput);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findAlert = () => wrapper.findComponent(GlAlert);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
expect(element.attributes('disabled')).toBe('true');
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
};
const createMockApolloProvider = ({ mutationResponse }) => {
createResponse = jest.fn().mockResolvedValue(mutationResponse);
return createMockApollo([[createNewAgentToken, createResponse]]);
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getClusterAgentQuery,
data: getTokenResponse.data,
variables: {
agentName,
projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
};
const createWrapper = () => {
wrapper = shallowMountExtended(CreateTokenModal, {
apolloProvider,
provide,
propsData,
stubs: {
GlModal,
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const mockCreatedResponse = (mutationResponse) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
writeQuery();
createWrapper();
findInput().vm.$emit('input', 'new-token');
findTextarea().vm.$emit('input', 'new-token-description');
findActionButton().vm.$emit('click');
return waitForPromises();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
describe('initial state', () => {
it('renders an input for the token name', () => {
expect(findInput().exists()).toBe(true);
expectDisabledAttribute(findInput(), false);
expect(findInput().attributes('max-length')).toBe(TOKEN_NAME_LIMIT.toString());
});
it('renders a textarea for the token description', () => {
expect(findTextarea().exists()).toBe(true);
expectDisabledAttribute(findTextarea(), false);
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
});
it('sends tracking event for modal shown', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
});
});
});
describe('when user inputs the token name', () => {
beforeEach(() => {
expectDisabledAttribute(findActionButton(), true);
findInput().vm.$emit('input', 'new-token');
});
it('enables the next button', () => {
expectDisabledAttribute(findActionButton(), false);
});
});
describe('when user clicks the create-token button', () => {
beforeEach(async () => {
const loadingResponse = new Promise(() => {});
await mockCreatedResponse(loadingResponse);
findInput().vm.$emit('input', 'new-token');
findActionButton().vm.$emit('click');
});
it('disables the create-token button', () => {
expectDisabledAttribute(findActionButton(), true);
});
it('hides the cancel button', () => {
expect(findCancelButton().exists()).toBe(false);
});
});
describe('creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(clusterAgentToken);
});
it('creates a token', () => {
expect(createResponse).toHaveBeenCalledWith({
input: { clusterAgentId, name: 'new-token', description: 'new-token-description' },
});
});
it('shows agent instructions', () => {
expect(findAgentInstructions().props()).toMatchObject({
agentName,
agentToken: 'token-secret',
modalId: CREATE_TOKEN_MODAL,
});
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe('Close');
expectDisabledAttribute(findActionButton(), false);
});
});
describe('error creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
});

View File

@ -2,6 +2,7 @@ import { GlEmptyState, GlTooltip, GlTruncate } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import TokenTable from '~/clusters/agents/components/token_table.vue';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import CreateTokenModal from '~/clusters/agents/components/create_token_modal.vue';
import { useFakeDate } from 'helpers/fake_date';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MAX_LIST_COUNT } from '~/clusters/agents/constants';
@ -50,6 +51,7 @@ describe('ClusterAgentTokenTable', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCreateTokenBtn = () => wrapper.findComponent(CreateTokenButton);
const findCreateModal = () => wrapper.findComponent(CreateTokenModal);
beforeEach(() => {
return createComponent(defaultTokens);
@ -63,8 +65,8 @@ describe('ClusterAgentTokenTable', () => {
expect(findCreateTokenBtn().exists()).toBe(true);
});
it('passes the correct params to the create token component', () => {
expect(findCreateTokenBtn().props()).toMatchObject({
it('passes the correct params to the create token modal component', () => {
expect(findCreateModal().props()).toMatchObject({
clusterAgentId,
cursor,
});

View File

@ -127,8 +127,8 @@ describe('Client side Markdown processing', () => {
pristineDoc: document,
});
const sourceAttrs = (sourceMapKey, sourceMarkdown) => ({
sourceMapKey,
const source = (sourceMarkdown) => ({
sourceMapKey: expect.any(String),
sourceMarkdown,
});
@ -136,63 +136,48 @@ describe('Client side Markdown processing', () => {
{
markdown: '__bold text__',
expectedDoc: doc(
paragraph(
sourceAttrs('0:13', '__bold text__'),
bold(sourceAttrs('0:13', '__bold text__'), 'bold text'),
),
paragraph(source('__bold text__'), bold(source('__bold text__'), 'bold text')),
),
},
{
markdown: '**bold text**',
expectedDoc: doc(
paragraph(
sourceAttrs('0:13', '**bold text**'),
bold(sourceAttrs('0:13', '**bold text**'), 'bold text'),
),
paragraph(source('**bold text**'), bold(source('**bold text**'), 'bold text')),
),
},
{
markdown: '<strong>bold text</strong>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:26', '<strong>bold text</strong>'),
bold(sourceAttrs('0:26', '<strong>bold text</strong>'), 'bold text'),
source('<strong>bold text</strong>'),
bold(source('<strong>bold text</strong>'), 'bold text'),
),
),
},
{
markdown: '<b>bold text</b>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:16', '<b>bold text</b>'),
bold(sourceAttrs('0:16', '<b>bold text</b>'), 'bold text'),
),
paragraph(source('<b>bold text</b>'), bold(source('<b>bold text</b>'), 'bold text')),
),
},
{
markdown: '_italic text_',
expectedDoc: doc(
paragraph(
sourceAttrs('0:13', '_italic text_'),
italic(sourceAttrs('0:13', '_italic text_'), 'italic text'),
),
paragraph(source('_italic text_'), italic(source('_italic text_'), 'italic text')),
),
},
{
markdown: '*italic text*',
expectedDoc: doc(
paragraph(
sourceAttrs('0:13', '*italic text*'),
italic(sourceAttrs('0:13', '*italic text*'), 'italic text'),
),
paragraph(source('*italic text*'), italic(source('*italic text*'), 'italic text')),
),
},
{
markdown: '<em>italic text</em>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:20', '<em>italic text</em>'),
italic(sourceAttrs('0:20', '<em>italic text</em>'), 'italic text'),
source('<em>italic text</em>'),
italic(source('<em>italic text</em>'), 'italic text'),
),
),
},
@ -200,28 +185,25 @@ describe('Client side Markdown processing', () => {
markdown: '<i>italic text</i>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:18', '<i>italic text</i>'),
italic(sourceAttrs('0:18', '<i>italic text</i>'), 'italic text'),
source('<i>italic text</i>'),
italic(source('<i>italic text</i>'), 'italic text'),
),
),
},
{
markdown: '`inline code`',
expectedDoc: doc(
paragraph(
sourceAttrs('0:13', '`inline code`'),
code(sourceAttrs('0:13', '`inline code`'), 'inline code'),
),
paragraph(source('`inline code`'), code(source('`inline code`'), 'inline code')),
),
},
{
markdown: '**`inline code bold`**',
expectedDoc: doc(
paragraph(
sourceAttrs('0:22', '**`inline code bold`**'),
source('**`inline code bold`**'),
bold(
sourceAttrs('0:22', '**`inline code bold`**'),
code(sourceAttrs('2:20', '`inline code bold`'), 'inline code bold'),
source('**`inline code bold`**'),
code(source('`inline code bold`'), 'inline code bold'),
),
),
),
@ -230,10 +212,10 @@ describe('Client side Markdown processing', () => {
markdown: '_`inline code italics`_',
expectedDoc: doc(
paragraph(
sourceAttrs('0:23', '_`inline code italics`_'),
source('_`inline code italics`_'),
italic(
sourceAttrs('0:23', '_`inline code italics`_'),
code(sourceAttrs('1:22', '`inline code italics`'), 'inline code italics'),
source('_`inline code italics`_'),
code(source('`inline code italics`'), 'inline code italics'),
),
),
),
@ -246,8 +228,8 @@ describe('Client side Markdown processing', () => {
`,
expectedDoc: doc(
paragraph(
sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'),
italic(sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'),
source('<i class="foo">\n *bar*\n</i>'),
italic(source('<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'),
),
),
},
@ -259,8 +241,8 @@ describe('Client side Markdown processing', () => {
`,
expectedDoc: doc(
paragraph(
sourceAttrs('0:27', '<img src="bar" alt="foo" />'),
image({ ...sourceAttrs('0:27', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
source('<img src="bar" alt="foo" />'),
image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
),
),
},
@ -273,15 +255,12 @@ describe('Client side Markdown processing', () => {
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:13', '- List item 1'),
listItem(
sourceAttrs('0:13', '- List item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
source('- List item 1'),
listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
),
paragraph(
sourceAttrs('15:42', '<img src="bar" alt="foo" />'),
image({ ...sourceAttrs('15:42', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
source('<img src="bar" alt="foo" />'),
image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
),
),
},
@ -289,10 +268,10 @@ describe('Client side Markdown processing', () => {
markdown: '[GitLab](https://gitlab.com "Go to GitLab")',
expectedDoc: doc(
paragraph(
sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'),
source('[GitLab](https://gitlab.com "Go to GitLab")'),
link(
{
...sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'),
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
title: 'Go to GitLab',
},
@ -305,12 +284,12 @@ describe('Client side Markdown processing', () => {
markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**',
expectedDoc: doc(
paragraph(
sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'),
source('**[GitLab](https://gitlab.com "Go to GitLab")**'),
bold(
sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'),
source('**[GitLab](https://gitlab.com "Go to GitLab")**'),
link(
{
...sourceAttrs('2:45', '[GitLab](https://gitlab.com "Go to GitLab")'),
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
title: 'Go to GitLab',
},
@ -324,10 +303,10 @@ describe('Client side Markdown processing', () => {
markdown: 'www.commonmark.org',
expectedDoc: doc(
paragraph(
sourceAttrs('0:18', 'www.commonmark.org'),
source('www.commonmark.org'),
link(
{
...sourceAttrs('0:18', 'www.commonmark.org'),
...source('www.commonmark.org'),
href: 'http://www.commonmark.org',
},
'www.commonmark.org',
@ -339,11 +318,11 @@ describe('Client side Markdown processing', () => {
markdown: 'Visit www.commonmark.org/help for more information.',
expectedDoc: doc(
paragraph(
sourceAttrs('0:51', 'Visit www.commonmark.org/help for more information.'),
source('Visit www.commonmark.org/help for more information.'),
'Visit ',
link(
{
...sourceAttrs('6:29', 'www.commonmark.org/help'),
...source('www.commonmark.org/help'),
href: 'http://www.commonmark.org/help',
},
'www.commonmark.org/help',
@ -356,11 +335,11 @@ describe('Client side Markdown processing', () => {
markdown: 'hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.',
expectedDoc: doc(
paragraph(
sourceAttrs('0:66', 'hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.'),
source('hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.'),
'hello@mail+xyz.example isnt valid, but ',
link(
{
...sourceAttrs('40:62', 'hello+xyz@mail.example'),
...source('hello+xyz@mail.example'),
href: 'mailto:hello+xyz@mail.example',
},
'hello+xyz@mail.example',
@ -373,11 +352,12 @@ describe('Client side Markdown processing', () => {
markdown: '[https://gitlab.com>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:20', '[https://gitlab.com>'),
source('[https://gitlab.com>'),
'[',
link(
{
...sourceAttrs(),
sourceMapKey: null,
sourceMarkdown: null,
href: 'https://gitlab.com',
},
'https://gitlab.com',
@ -392,9 +372,9 @@ This is a paragraph with a\\
hard line break`,
expectedDoc: doc(
paragraph(
sourceAttrs('0:43', 'This is a paragraph with a\\\nhard line break'),
source('This is a paragraph with a\\\nhard line break'),
'This is a paragraph with a',
hardBreak(sourceAttrs('26:28', '\\\n')),
hardBreak(source('\\\n')),
'\nhard line break',
),
),
@ -403,9 +383,9 @@ hard line break`,
markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")',
expectedDoc: doc(
paragraph(
sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
image({
...sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
...source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
alt: 'GitLab Logo',
src: 'https://gitlab.com/logo.png',
title: 'GitLab Logo',
@ -415,49 +395,43 @@ hard line break`,
},
{
markdown: '---',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '---'))),
expectedDoc: doc(horizontalRule(source('---'))),
},
{
markdown: '***',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '***'))),
expectedDoc: doc(horizontalRule(source('***'))),
},
{
markdown: '___',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '___'))),
expectedDoc: doc(horizontalRule(source('___'))),
},
{
markdown: '<hr>',
expectedDoc: doc(horizontalRule(sourceAttrs('0:4', '<hr>'))),
expectedDoc: doc(horizontalRule(source('<hr>'))),
},
{
markdown: '# Heading 1',
expectedDoc: doc(heading({ ...sourceAttrs('0:11', '# Heading 1'), level: 1 }, 'Heading 1')),
expectedDoc: doc(heading({ ...source('# Heading 1'), level: 1 }, 'Heading 1')),
},
{
markdown: '## Heading 2',
expectedDoc: doc(heading({ ...sourceAttrs('0:12', '## Heading 2'), level: 2 }, 'Heading 2')),
expectedDoc: doc(heading({ ...source('## Heading 2'), level: 2 }, 'Heading 2')),
},
{
markdown: '### Heading 3',
expectedDoc: doc(heading({ ...sourceAttrs('0:13', '### Heading 3'), level: 3 }, 'Heading 3')),
expectedDoc: doc(heading({ ...source('### Heading 3'), level: 3 }, 'Heading 3')),
},
{
markdown: '#### Heading 4',
expectedDoc: doc(
heading({ ...sourceAttrs('0:14', '#### Heading 4'), level: 4 }, 'Heading 4'),
),
expectedDoc: doc(heading({ ...source('#### Heading 4'), level: 4 }, 'Heading 4')),
},
{
markdown: '##### Heading 5',
expectedDoc: doc(
heading({ ...sourceAttrs('0:15', '##### Heading 5'), level: 5 }, 'Heading 5'),
),
expectedDoc: doc(heading({ ...source('##### Heading 5'), level: 5 }, 'Heading 5')),
},
{
markdown: '###### Heading 6',
expectedDoc: doc(
heading({ ...sourceAttrs('0:16', '###### Heading 6'), level: 6 }, 'Heading 6'),
),
expectedDoc: doc(heading({ ...source('###### Heading 6'), level: 6 }, 'Heading 6')),
},
{
markdown: `
@ -465,9 +439,7 @@ Heading
one
======
`,
expectedDoc: doc(
heading({ ...sourceAttrs('0:18', 'Heading\none\n======'), level: 1 }, 'Heading\none'),
),
expectedDoc: doc(heading({ ...source('Heading\none\n======'), level: 1 }, 'Heading\none')),
},
{
markdown: `
@ -475,9 +447,7 @@ Heading
two
-------
`,
expectedDoc: doc(
heading({ ...sourceAttrs('0:19', 'Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo'),
),
expectedDoc: doc(heading({ ...source('Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo')),
},
{
markdown: `
@ -486,15 +456,9 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:27', '- List item 1\n- List item 2'),
listItem(
sourceAttrs('0:13', '- List item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '- List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
source('- List item 1\n- List item 2'),
listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -505,15 +469,9 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:27', '* List item 1\n* List item 2'),
listItem(
sourceAttrs('0:13', '* List item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '* List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
source('* List item 1\n* List item 2'),
listItem(source('* List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('* List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -524,15 +482,9 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:27', '+ List item 1\n+ List item 2'),
listItem(
sourceAttrs('0:13', '+ List item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '+ List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
source('+ List item 1\n+ List item 2'),
listItem(source('+ List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('+ List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -543,15 +495,9 @@ two
`,
expectedDoc: doc(
orderedList(
sourceAttrs('0:29', '1. List item 1\n1. List item 2'),
listItem(
sourceAttrs('0:14', '1. List item 1'),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '1. List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
source('1. List item 1\n1. List item 2'),
listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('1. List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -562,15 +508,9 @@ two
`,
expectedDoc: doc(
orderedList(
sourceAttrs('0:29', '1. List item 1\n2. List item 2'),
listItem(
sourceAttrs('0:14', '1. List item 1'),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '2. List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
source('1. List item 1\n2. List item 2'),
listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('2. List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -581,15 +521,9 @@ two
`,
expectedDoc: doc(
orderedList(
sourceAttrs('0:29', '1) List item 1\n2) List item 2'),
listItem(
sourceAttrs('0:14', '1) List item 1'),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '2) List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
source('1) List item 1\n2) List item 2'),
listItem(source('1) List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('2) List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -600,15 +534,15 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:33', '- List item 1\n - Sub list item 1'),
source('- List item 1\n - Sub list item 1'),
listItem(
sourceAttrs('0:33', '- List item 1\n - Sub list item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
source('- List item 1\n - Sub list item 1'),
paragraph(source('List item 1'), 'List item 1'),
bulletList(
sourceAttrs('16:33', '- Sub list item 1'),
source('- Sub list item 1'),
listItem(
sourceAttrs('16:33', '- Sub list item 1'),
paragraph(sourceAttrs('18:33', 'Sub list item 1'), 'Sub list item 1'),
source('- Sub list item 1'),
paragraph(source('Sub list item 1'), 'Sub list item 1'),
),
),
),
@ -624,19 +558,13 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs(
'0:66',
'- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2',
),
source('- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2'),
listItem(
sourceAttrs('0:52', '- List item 1 paragraph 1\n\n List item 1 paragraph 2'),
paragraph(sourceAttrs('2:25', 'List item 1 paragraph 1'), 'List item 1 paragraph 1'),
paragraph(sourceAttrs('29:52', 'List item 1 paragraph 2'), 'List item 1 paragraph 2'),
),
listItem(
sourceAttrs('53:66', '- List item 2'),
paragraph(sourceAttrs('55:66', 'List item 2'), 'List item 2'),
source('- List item 1 paragraph 1\n\n List item 1 paragraph 2'),
paragraph(source('List item 1 paragraph 1'), 'List item 1 paragraph 1'),
paragraph(source('List item 1 paragraph 2'), 'List item 1 paragraph 2'),
),
listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
},
@ -646,13 +574,13 @@ two
`,
expectedDoc: doc(
bulletList(
sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'),
source('- List item with an image ![bar](foo.png)'),
listItem(
sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'),
source('- List item with an image ![bar](foo.png)'),
paragraph(
sourceAttrs('2:41', 'List item with an image ![bar](foo.png)'),
source('List item with an image ![bar](foo.png)'),
'List item with an image',
image({ ...sourceAttrs('26:41', '![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
image({ ...source('![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
),
),
),
@ -664,8 +592,8 @@ two
`,
expectedDoc: doc(
blockquote(
sourceAttrs('0:22', '> This is a blockquote'),
paragraph(sourceAttrs('2:22', 'This is a blockquote'), 'This is a blockquote'),
source('> This is a blockquote'),
paragraph(source('This is a blockquote'), 'This is a blockquote'),
),
),
},
@ -676,17 +604,11 @@ two
`,
expectedDoc: doc(
blockquote(
sourceAttrs('0:31', '> - List item 1\n> - List item 2'),
source('> - List item 1\n> - List item 2'),
bulletList(
sourceAttrs('2:31', '- List item 1\n> - List item 2'),
listItem(
sourceAttrs('2:15', '- List item 1'),
paragraph(sourceAttrs('4:15', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('18:31', '- List item 2'),
paragraph(sourceAttrs('20:31', 'List item 2'), 'List item 2'),
),
source('- List item 1\n> - List item 2'),
listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
),
),
),
@ -699,10 +621,10 @@ code block
`,
expectedDoc: doc(
paragraph(sourceAttrs('0:10', 'code block'), 'code block'),
paragraph(source('code block'), 'code block'),
codeBlock(
{
...sourceAttrs('12:42', " const fn = () => 'GitLab';"),
...source(" const fn = () => 'GitLab';"),
class: 'code highlight',
language: null,
},
@ -719,7 +641,7 @@ const fn = () => 'GitLab';
expectedDoc: doc(
codeBlock(
{
...sourceAttrs('0:44', "```javascript\nconst fn = () => 'GitLab';\n```"),
...source("```javascript\nconst fn = () => 'GitLab';\n```"),
class: 'code highlight',
language: 'javascript',
},
@ -736,7 +658,7 @@ const fn = () => 'GitLab';
expectedDoc: doc(
codeBlock(
{
...sourceAttrs('0:44', "~~~javascript\nconst fn = () => 'GitLab';\n~~~"),
...source("~~~javascript\nconst fn = () => 'GitLab';\n~~~"),
class: 'code highlight',
language: 'javascript',
},
@ -752,7 +674,7 @@ const fn = () => 'GitLab';
expectedDoc: doc(
codeBlock(
{
...sourceAttrs('0:7', '```\n```'),
...source('```\n```'),
class: 'code highlight',
language: null,
},
@ -770,7 +692,7 @@ const fn = () => 'GitLab';
expectedDoc: doc(
codeBlock(
{
...sourceAttrs('0:45', "```javascript\nconst fn = () => 'GitLab';\n\n```"),
...source("```javascript\nconst fn = () => 'GitLab';\n\n```"),
class: 'code highlight',
language: 'javascript',
},
@ -782,8 +704,8 @@ const fn = () => 'GitLab';
markdown: '~~Strikedthrough text~~',
expectedDoc: doc(
paragraph(
sourceAttrs('0:23', '~~Strikedthrough text~~'),
strike(sourceAttrs('0:23', '~~Strikedthrough text~~'), 'Strikedthrough text'),
source('~~Strikedthrough text~~'),
strike(source('~~Strikedthrough text~~'), 'Strikedthrough text'),
),
),
},
@ -791,8 +713,8 @@ const fn = () => 'GitLab';
markdown: '<del>Strikedthrough text</del>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:30', '<del>Strikedthrough text</del>'),
strike(sourceAttrs('0:30', '<del>Strikedthrough text</del>'), 'Strikedthrough text'),
source('<del>Strikedthrough text</del>'),
strike(source('<del>Strikedthrough text</del>'), 'Strikedthrough text'),
),
),
},
@ -800,11 +722,8 @@ const fn = () => 'GitLab';
markdown: '<strike>Strikedthrough text</strike>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'),
strike(
sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'),
'Strikedthrough text',
),
source('<strike>Strikedthrough text</strike>'),
strike(source('<strike>Strikedthrough text</strike>'), 'Strikedthrough text'),
),
),
},
@ -812,8 +731,8 @@ const fn = () => 'GitLab';
markdown: '<s>Strikedthrough text</s>',
expectedDoc: doc(
paragraph(
sourceAttrs('0:26', '<s>Strikedthrough text</s>'),
strike(sourceAttrs('0:26', '<s>Strikedthrough text</s>'), 'Strikedthrough text'),
source('<s>Strikedthrough text</s>'),
strike(source('<s>Strikedthrough text</s>'), 'Strikedthrough text'),
),
),
},
@ -826,21 +745,21 @@ const fn = () => 'GitLab';
taskList(
{
numeric: false,
...sourceAttrs('0:45', '- [ ] task list item 1\n- [ ] task list item 2'),
...source('- [ ] task list item 1\n- [ ] task list item 2'),
},
taskItem(
{
checked: false,
...sourceAttrs('0:22', '- [ ] task list item 1'),
...source('- [ ] task list item 1'),
},
paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'),
paragraph(source('task list item 1'), 'task list item 1'),
),
taskItem(
{
checked: false,
...sourceAttrs('23:45', '- [ ] task list item 2'),
...source('- [ ] task list item 2'),
},
paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'),
paragraph(source('task list item 2'), 'task list item 2'),
),
),
),
@ -854,21 +773,21 @@ const fn = () => 'GitLab';
taskList(
{
numeric: false,
...sourceAttrs('0:45', '- [x] task list item 1\n- [x] task list item 2'),
...source('- [x] task list item 1\n- [x] task list item 2'),
},
taskItem(
{
checked: true,
...sourceAttrs('0:22', '- [x] task list item 1'),
...source('- [x] task list item 1'),
},
paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'),
paragraph(source('task list item 1'), 'task list item 1'),
),
taskItem(
{
checked: true,
...sourceAttrs('23:45', '- [x] task list item 2'),
...source('- [x] task list item 2'),
},
paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'),
paragraph(source('task list item 2'), 'task list item 2'),
),
),
),
@ -882,21 +801,21 @@ const fn = () => 'GitLab';
taskList(
{
numeric: true,
...sourceAttrs('0:47', '1. [ ] task list item 1\n2. [ ] task list item 2'),
...source('1. [ ] task list item 1\n2. [ ] task list item 2'),
},
taskItem(
{
checked: false,
...sourceAttrs('0:23', '1. [ ] task list item 1'),
...source('1. [ ] task list item 1'),
},
paragraph(sourceAttrs('7:23', 'task list item 1'), 'task list item 1'),
paragraph(source('task list item 1'), 'task list item 1'),
),
taskItem(
{
checked: false,
...sourceAttrs('24:47', '2. [ ] task list item 2'),
...source('2. [ ] task list item 2'),
},
paragraph(sourceAttrs('31:47', 'task list item 2'), 'task list item 2'),
paragraph(source('task list item 2'), 'task list item 2'),
),
),
),
@ -909,16 +828,16 @@ const fn = () => 'GitLab';
`,
expectedDoc: doc(
table(
sourceAttrs('0:29', '| a | b |\n|---|---|\n| c | d |'),
source('| a | b |\n|---|---|\n| c | d |'),
tableRow(
sourceAttrs('0:9', '| a | b |'),
tableHeader(sourceAttrs('0:5', '| a |'), paragraph(sourceAttrs('2:3', 'a'), 'a')),
tableHeader(sourceAttrs('5:9', ' b |'), paragraph(sourceAttrs('6:7', 'b'), 'b')),
source('| a | b |'),
tableHeader(source('| a |'), paragraph(source('a'), 'a')),
tableHeader(source(' b |'), paragraph(source('b'), 'b')),
),
tableRow(
sourceAttrs('20:29', '| c | d |'),
tableCell(sourceAttrs('20:25', '| c |'), paragraph(sourceAttrs('22:23', 'c'), 'c')),
tableCell(sourceAttrs('25:29', ' d |'), paragraph(sourceAttrs('26:27', 'd'), 'd')),
source('| c | d |'),
tableCell(source('| c |'), paragraph(source('c'), 'c')),
tableCell(source(' d |'), paragraph(source('d'), 'd')),
),
),
),
@ -936,30 +855,29 @@ const fn = () => 'GitLab';
`,
expectedDoc: doc(
table(
sourceAttrs(
'0:132',
source(
'<table>\n <tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>\n <tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>\n</table>',
),
tableRow(
sourceAttrs('10:66', '<tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>'),
source('<tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>'),
tableHeader(
{
...sourceAttrs('19:58', '<th colspan="2" rowspan="5">Header</th>'),
...source('<th colspan="2" rowspan="5">Header</th>'),
colspan: 2,
rowspan: 5,
},
paragraph(sourceAttrs('47:53', 'Header'), 'Header'),
paragraph(source('Header'), 'Header'),
),
),
tableRow(
sourceAttrs('69:123', '<tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>'),
source('<tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>'),
tableCell(
{
...sourceAttrs('78:115', '<td colspan="2" rowspan="5">Body</td>'),
...source('<td colspan="2" rowspan="5">Body</td>'),
colspan: 2,
rowspan: 5,
},
paragraph(sourceAttrs('106:110', 'Body'), 'Body'),
paragraph(source('Body'), 'Body'),
),
),
),
@ -977,24 +895,24 @@ Paragraph
`,
expectedDoc: doc(
paragraph(
sourceAttrs('0:30', 'This is a footnote [^footnote]'),
source('This is a footnote [^footnote]'),
'This is a footnote ',
footnoteReference({
...sourceAttrs('19:30', '[^footnote]'),
...source('[^footnote]'),
identifier: 'footnote',
label: 'footnote',
}),
),
paragraph(sourceAttrs('32:41', 'Paragraph'), 'Paragraph'),
paragraph(source('Paragraph'), 'Paragraph'),
footnoteDefinition(
{
...sourceAttrs('43:75', '[^footnote]: Footnote definition'),
...source('[^footnote]: Footnote definition'),
identifier: 'footnote',
label: 'footnote',
},
paragraph(sourceAttrs('56:75', 'Footnote definition'), 'Footnote definition'),
paragraph(source('Footnote definition'), 'Footnote definition'),
),
paragraph(sourceAttrs('77:86', 'Paragraph'), 'Paragraph'),
paragraph(source('Paragraph'), 'Paragraph'),
),
},
];

View File

@ -7,7 +7,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
it 'returns the object kind for a deployment' do
deployment = build(:deployment, deployable: nil, environment: create(:environment))
data = described_class.build(deployment, Time.current)
data = described_class.build(deployment, 'success', Time.current)
expect(data[:object_kind]).to eq('deployment')
end
@ -23,7 +23,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expected_commit_url = Gitlab::UrlBuilder.build(commit)
status_changed_at = Time.current
data = described_class.build(deployment, status_changed_at)
data = described_class.build(deployment, 'failed', status_changed_at)
expect(data[:status]).to eq('failed')
expect(data[:status_changed_at]).to eq(status_changed_at)
@ -42,7 +42,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
it 'does not include the deployable URL when there is no deployable' do
deployment = create(:deployment, status: :failed, deployable: nil)
data = described_class.build(deployment, Time.current)
data = described_class.build(deployment, 'failed', Time.current)
expect(data[:deployable_url]).to be_nil
end
@ -51,7 +51,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:deployment) { create(:deployment, project: project) }
subject(:data) { described_class.build(deployment, Time.current) }
subject(:data) { described_class.build(deployment, 'created', Time.current) }
before(:all) do
project.repository.remove
@ -69,7 +69,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
context 'when deployed_by is nil' do
let_it_be(:deployment) { create(:deployment, user: nil, deployable: nil) }
subject(:data) { described_class.build(deployment, Time.current) }
subject(:data) { described_class.build(deployment, 'created', Time.current) }
before(:all) do
deployment.user = nil

View File

@ -1364,7 +1364,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks)
allow(Deployments::HooksWorker).to receive(:perform_async)
end
it 'has deployments record with created status' do
@ -1420,7 +1420,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks)
allow(Deployments::HooksWorker).to receive(:perform_async)
end
it_behaves_like 'avoid deadlock'
@ -1506,28 +1506,14 @@ RSpec.describe Ci::Build do
it 'transitions to running and calls webhook' do
freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current)
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
subject
end
expect(deployment).to be_running
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
subject
end
end
end
end
end
end

View File

@ -139,29 +139,16 @@ RSpec.describe Deployment do
end
end
it 'executes deployment hooks' do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current)
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'running',
status_changed_at: Time.current)
deployment.run!
end
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.run!
end
end
end
it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do
expect(Deployments::DropOlderDeploymentsWorker)
.to receive(:perform_async).once.with(deployment.id)
@ -189,28 +176,15 @@ RSpec.describe Deployment do
deployment.succeed!
end
it 'executes deployment hooks' do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current)
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'success',
status_changed_at: Time.current)
deployment.succeed!
end
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.succeed!
end
end
end
end
context 'when deployment failed' do
@ -232,28 +206,15 @@ RSpec.describe Deployment do
deployment.drop!
end
it 'executes deployment hooks' do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current)
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'failed',
status_changed_at: Time.current)
deployment.drop!
end
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.drop!
end
end
end
end
context 'when deployment was canceled' do
@ -275,28 +236,15 @@ RSpec.describe Deployment do
deployment.cancel!
end
it 'executes deployment hooks' do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current)
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'canceled',
status_changed_at: Time.current)
deployment.cancel!
end
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.cancel!
end
end
end
end
context 'when deployment was skipped' do
@ -324,12 +272,6 @@ RSpec.describe Deployment do
deployment.skip!
end
end
it 'does not execute deployment hooks' do
expect(deployment).not_to receive(:execute_hooks)
deployment.skip!
end
end
context 'when deployment is blocked' do
@ -353,12 +295,6 @@ RSpec.describe Deployment do
deployment.block!
end
it 'does not execute deployment hooks' do
expect(deployment).not_to receive(:execute_hooks)
deployment.block!
end
end
describe 'synching status to Jira' do
@ -1052,30 +988,11 @@ RSpec.describe Deployment do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async)
expect(Deployments::HooksWorker).to receive(:perform_async)
expect(deploy.update_status('success')).to eq(true)
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'schedules `Deployments::HooksWorker` when finishing a deploy' do
expect(Deployments::HooksWorker).to receive(:perform_async)
deploy.update_status('success')
end
end
it 'executes deployment hooks when finishing a deploy' do
freeze_time do
expect(deploy).to receive(:execute_hooks).with(Time.current)
deploy.update_status('success')
end
end
it 'updates finished_at when transitioning to a finished status' do
freeze_time do
deploy.update_status('success')

View File

@ -14,7 +14,7 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do
let_it_be(:deployment) { create(:deployment, status: :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha) }
let(:args) do
Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
Gitlab::DataBuilder::Deployment.build(deployment, 'success', Time.current)
end
it_behaves_like Integrations::ChatMessage

View File

@ -59,7 +59,7 @@ RSpec.describe Integrations::Slack do
context 'deployment notification' do
let_it_be(:deployment) { create(:deployment, user: user) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) }
it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
end

View File

@ -5,6 +5,7 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
RSpec.describe API::Markdown, 'Snapshot' do
# noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil)
glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__)
glfm_example_snapshots_dir = File.expand_path('../../fixtures/glfm/example_snapshots', __dir__)
include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir, glfm_example_snapshots_dir

View File

@ -14,7 +14,7 @@ require_relative '../../../../scripts/lib/glfm/update_example_snapshots'
# This is because the invocation of the full script is slow, because it executes
# two subshells for processing, one which runs a full Rails environment, and one
# which runs a jest test environment. This results in each full run of the script
# taking between 30-60 seconds. The majority of this is spent loading the Rails environmnent.
# taking between 30-60 seconds. The majority of this is spent loading the Rails environment.
#
# However, only the `writing html.yml and prosemirror_json.yml` context is used
# to test these slow sub-processes, and it only contains a single example.

View File

@ -21,34 +21,11 @@ RSpec.describe Deployments::CreateService do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect_next_instance_of(Deployment) do |deployment|
expect(deployment).to receive(:execute_hooks)
end
expect(Deployments::HooksWorker).to receive(:perform_async)
expect(service.execute).to be_persisted
end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
service = described_class.new(
environment,
user,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
)
expect(Deployments::HooksWorker).to receive(:perform_async)
service.execute
end
end
it 'does not change the status if no status is given' do
service = described_class.new(
environment,
@ -60,9 +37,7 @@ RSpec.describe Deployments::CreateService do
expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
expect_next_instance_of(Deployment) do |deployment|
expect(deployment).not_to receive(:execute_hooks)
end
expect(Deployments::HooksWorker).not_to receive(:perform_async)
expect(service.execute).to be_persisted
end
@ -80,9 +55,11 @@ RSpec.describe Deployments::CreateService do
it 'does not create a new deployment' do
described_class.new(environment, user, params).execute
expect do
described_class.new(environment.reload, user, params).execute
end.not_to change { Deployment.count }
expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
expect(Deployments::HooksWorker).not_to receive(:perform_async)
described_class.new(environment.reload, user, params).execute
end
end
end

View File

@ -33,7 +33,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do
before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks)
allow(Deployments::HooksWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end

View File

@ -357,7 +357,8 @@ RSpec.shared_examples "chat integration" do |integration_name|
end
context 'deployment events' do
let(:sample_data) { Gitlab::DataBuilder::Deployment.build(create(:deployment), Time.now) }
let(:deployment) { create(:deployment) }
let(:sample_data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) }
it_behaves_like "untriggered #{integration_name} integration"
end

View File

@ -230,7 +230,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'deployment events' do
let_it_be(:deployment) { create(:deployment) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, 'created', Time.current) }
it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/
end
@ -677,7 +677,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch)
end
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) }
before do
allow(chat_integration).to receive_messages(

Binary file not shown.