Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
37699393e9
commit
feb61d56e7
|
@ -55,10 +55,7 @@ Graphql/ResolverType:
|
||||||
- 'app/graphql/resolvers/merge_requests_resolver.rb'
|
- 'app/graphql/resolvers/merge_requests_resolver.rb'
|
||||||
- 'app/graphql/resolvers/project_merge_requests_resolver.rb'
|
- 'app/graphql/resolvers/project_merge_requests_resolver.rb'
|
||||||
- 'app/graphql/resolvers/project_pipelines_resolver.rb'
|
- 'app/graphql/resolvers/project_pipelines_resolver.rb'
|
||||||
- 'app/graphql/resolvers/projects/snippets_resolver.rb'
|
|
||||||
- 'app/graphql/resolvers/snippets_resolver.rb'
|
|
||||||
- 'app/graphql/resolvers/users/group_count_resolver.rb'
|
- 'app/graphql/resolvers/users/group_count_resolver.rb'
|
||||||
- 'app/graphql/resolvers/users/snippets_resolver.rb'
|
|
||||||
- 'ee/app/graphql/resolvers/ci/jobs_resolver.rb'
|
- 'ee/app/graphql/resolvers/ci/jobs_resolver.rb'
|
||||||
- 'ee/app/graphql/resolvers/geo/merge_request_diff_registries_resolver.rb'
|
- 'ee/app/graphql/resolvers/geo/merge_request_diff_registries_resolver.rb'
|
||||||
- 'ee/app/graphql/resolvers/geo/package_file_registries_resolver.rb'
|
- 'ee/app/graphql/resolvers/geo/package_file_registries_resolver.rb'
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
import Vue from 'vue';
|
||||||
import initUserInternalRegexPlaceholder from '../account_and_limits';
|
import initUserInternalRegexPlaceholder from '../account_and_limits';
|
||||||
|
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder());
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initUserInternalRegexPlaceholder();
|
||||||
|
|
||||||
|
const gitpodSettingEl = document.querySelector('#js-gitpod-settings-help-text');
|
||||||
|
if (!gitpodSettingEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue({
|
||||||
|
el: gitpodSettingEl,
|
||||||
|
name: 'GitpodSettings',
|
||||||
|
components: {
|
||||||
|
IntegrationHelpText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initProfilePreferences);
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script>
|
||||||
|
import { GlFormText, GlIcon, GlLink } from '@gitlab/ui';
|
||||||
|
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegrationView',
|
||||||
|
components: {
|
||||||
|
GlFormText,
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
IntegrationHelpText,
|
||||||
|
},
|
||||||
|
inject: ['userFields'],
|
||||||
|
props: {
|
||||||
|
helpLink: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
messageUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEnabled: this.userFields[this.config.formName],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formName() {
|
||||||
|
return `user[${this.config.formName}]`;
|
||||||
|
},
|
||||||
|
formId() {
|
||||||
|
return `user_${this.config.formName}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<label class="label-bold">
|
||||||
|
{{ config.title }}
|
||||||
|
</label>
|
||||||
|
<gl-link class="has-tooltip" title="More information" :href="helpLink">
|
||||||
|
<gl-icon name="question-o" class="vertical-align-middle" />
|
||||||
|
</gl-link>
|
||||||
|
<div class="form-group form-check" data-testid="profile-preferences-integration-form-group">
|
||||||
|
<!-- Necessary for Rails to receive the value when not checked -->
|
||||||
|
<input
|
||||||
|
:name="formName"
|
||||||
|
type="hidden"
|
||||||
|
value="0"
|
||||||
|
data-testid="profile-preferences-integration-hidden-field"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
:id="formId"
|
||||||
|
v-model="isEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
:name="formName"
|
||||||
|
value="1"
|
||||||
|
data-testid="profile-preferences-integration-checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="formId">
|
||||||
|
{{ config.label }}
|
||||||
|
</label>
|
||||||
|
<gl-form-text tag="div">
|
||||||
|
<integration-help-text :message="message" :message-url="messageUrl" />
|
||||||
|
</gl-form-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script>
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
import IntegrationView from './integration_view.vue';
|
||||||
|
|
||||||
|
const INTEGRATION_VIEW_CONFIGS = {
|
||||||
|
sourcegraph: {
|
||||||
|
title: s__('ProfilePreferences|Sourcegraph'),
|
||||||
|
label: s__('ProfilePreferences|Enable integrated code intelligence on code views'),
|
||||||
|
formName: 'sourcegraph_enabled',
|
||||||
|
},
|
||||||
|
gitpod: {
|
||||||
|
title: s__('ProfilePreferences|Gitpod'),
|
||||||
|
label: s__('ProfilePreferences|Enable Gitpod integration'),
|
||||||
|
formName: 'gitpod_enabled',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProfilePreferences',
|
||||||
|
components: {
|
||||||
|
IntegrationView,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
integrationViews: {
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
integrationViewConfigs: INTEGRATION_VIEW_CONFIGS,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row gl-mt-3 js-preferences-form">
|
||||||
|
<div v-if="integrationViews.length" class="col-sm-12">
|
||||||
|
<hr data-testid="profile-preferences-integrations-rule" />
|
||||||
|
</div>
|
||||||
|
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
|
||||||
|
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
|
||||||
|
{{ s__('ProfilePreferences|Integrations') }}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{{ s__('ProfilePreferences|Customize integrations with third party services.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="integrationViews.length" class="col-lg-8">
|
||||||
|
<integration-view
|
||||||
|
v-for="view in integrationViews"
|
||||||
|
:key="view.name"
|
||||||
|
:help-link="view.help_link"
|
||||||
|
:message="view.message"
|
||||||
|
:message-url="view.message_url"
|
||||||
|
:config="$options.integrationViewConfigs[view.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import ProfilePreferences from './components/profile_preferences.vue';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const el = document.querySelector('#js-profile-preferences-app');
|
||||||
|
const shouldParse = ['integrationViews', 'userFields'];
|
||||||
|
|
||||||
|
const provide = Object.keys(el.dataset).reduce((memo, key) => {
|
||||||
|
let value = el.dataset[key];
|
||||||
|
if (shouldParse.includes(key)) {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...memo, [key]: value };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
name: 'ProfilePreferencesApp',
|
||||||
|
provide,
|
||||||
|
render: createElement => createElement(ProfilePreferences),
|
||||||
|
});
|
||||||
|
};
|
|
@ -29,7 +29,6 @@ export default {
|
||||||
'markdownDocsPath',
|
'markdownDocsPath',
|
||||||
'markdownPreviewPath',
|
'markdownPreviewPath',
|
||||||
'releasesPagePath',
|
'releasesPagePath',
|
||||||
'updateReleaseApiDocsPath',
|
|
||||||
'release',
|
'release',
|
||||||
'newMilestonePath',
|
'newMilestonePath',
|
||||||
'manageMilestonesPath',
|
'manageMilestonesPath',
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
|
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||||
import FormFieldContainer from './form_field_container.vue';
|
import FormFieldContainer from './form_field_container.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TagFieldExisting',
|
name: 'TagFieldExisting',
|
||||||
components: { GlFormGroup, GlFormInput, GlSprintf, GlLink, FormFieldContainer },
|
components: { GlFormGroup, GlFormInput, FormFieldContainer },
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
|
...mapState('detail', ['release']),
|
||||||
inputId() {
|
inputId() {
|
||||||
return uniqueId('tag-name-input-');
|
return uniqueId('tag-name-input-');
|
||||||
},
|
},
|
||||||
|
@ -32,19 +32,7 @@ export default {
|
||||||
</form-field-container>
|
</form-field-container>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div :id="helpId" data-testid="tag-name-help">
|
<div :id="helpId" data-testid="tag-name-help">
|
||||||
<gl-sprintf
|
{{ __("The tag name can't be changed for an existing release.") }}
|
||||||
:message="
|
|
||||||
__(
|
|
||||||
'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #link="{ content }">
|
|
||||||
<gl-link :href="updateReleaseApiDocsPath" target="_blank">
|
|
||||||
{{ content }}
|
|
||||||
</gl-link>
|
|
||||||
</template>
|
|
||||||
</gl-sprintf>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</gl-form-group>
|
</gl-form-group>
|
||||||
|
|
|
@ -5,7 +5,6 @@ export default ({
|
||||||
projectPath,
|
projectPath,
|
||||||
markdownDocsPath,
|
markdownDocsPath,
|
||||||
markdownPreviewPath,
|
markdownPreviewPath,
|
||||||
updateReleaseApiDocsPath,
|
|
||||||
releaseAssetsDocsPath,
|
releaseAssetsDocsPath,
|
||||||
manageMilestonesPath,
|
manageMilestonesPath,
|
||||||
newMilestonePath,
|
newMilestonePath,
|
||||||
|
@ -20,7 +19,6 @@ export default ({
|
||||||
projectPath,
|
projectPath,
|
||||||
markdownDocsPath,
|
markdownDocsPath,
|
||||||
markdownPreviewPath,
|
markdownPreviewPath,
|
||||||
updateReleaseApiDocsPath,
|
|
||||||
releaseAssetsDocsPath,
|
releaseAssetsDocsPath,
|
||||||
manageMilestonesPath,
|
manageMilestonesPath,
|
||||||
newMilestonePath,
|
newMilestonePath,
|
||||||
|
|
|
@ -83,7 +83,13 @@ export default {
|
||||||
return this.editorMode === EDITOR_TYPES.wysiwyg;
|
return this.editorMode === EDITOR_TYPES.wysiwyg;
|
||||||
},
|
},
|
||||||
customRenderers() {
|
customRenderers() {
|
||||||
const imageRenderer = renderImage.build(this.mounts, this.project, this.branch, this.baseUrl);
|
const imageRenderer = renderImage.build(
|
||||||
|
this.mounts,
|
||||||
|
this.project,
|
||||||
|
this.branch,
|
||||||
|
this.baseUrl,
|
||||||
|
this.$options.imageRepository,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
image: [imageRenderer],
|
image: [imageRenderer],
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,9 +12,11 @@ const imageRepository = () => {
|
||||||
.catch(() => flash(__('Something went wrong while inserting your image. Please try again.')));
|
.catch(() => flash(__('Something went wrong while inserting your image. Please try again.')));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const get = path => images.get(path);
|
||||||
|
|
||||||
const getAll = () => images;
|
const getAll = () => images;
|
||||||
|
|
||||||
return { add, getAll };
|
return { add, get, getAll };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default imageRepository;
|
export default imageRepository;
|
||||||
|
|
|
@ -4,6 +4,8 @@ const canRender = ({ type }) => type === 'image';
|
||||||
|
|
||||||
let metadata;
|
let metadata;
|
||||||
|
|
||||||
|
const getCachedContent = basePath => metadata.imageRepository.get(basePath);
|
||||||
|
|
||||||
const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/');
|
const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/');
|
||||||
|
|
||||||
const extractSourceDirectory = url => {
|
const extractSourceDirectory = url => {
|
||||||
|
@ -46,7 +48,11 @@ const generateSourceDirectory = basePath => {
|
||||||
return sourceDir || defaultSourceDir;
|
return sourceDir || defaultSourceDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveFullPath = originalSrc => {
|
const resolveFullPath = (originalSrc, cachedContent) => {
|
||||||
|
if (cachedContent) {
|
||||||
|
return `data:image;base64,${cachedContent}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAbsolute(originalSrc)) {
|
if (isAbsolute(originalSrc)) {
|
||||||
return originalSrc;
|
return originalSrc;
|
||||||
}
|
}
|
||||||
|
@ -61,20 +67,22 @@ const resolveFullPath = originalSrc => {
|
||||||
const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => {
|
const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => {
|
||||||
skipChildren();
|
skipChildren();
|
||||||
|
|
||||||
|
const cachedContent = getCachedContent(originalSrc);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'openTag',
|
type: 'openTag',
|
||||||
tagName: 'img',
|
tagName: 'img',
|
||||||
selfClose: true,
|
selfClose: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
'data-original-src': !isAbsolute(originalSrc) ? originalSrc : '',
|
'data-original-src': !isAbsolute(originalSrc) || cachedContent ? originalSrc : '',
|
||||||
src: resolveFullPath(originalSrc),
|
src: resolveFullPath(originalSrc, cachedContent),
|
||||||
alt: firstChild.literal,
|
alt: firstChild.literal,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const build = (mounts = [], project, branch, baseUrl) => {
|
const build = (mounts = [], project, branch, baseUrl, imageRepository) => {
|
||||||
metadata = { mounts, project, branch, baseUrl };
|
metadata = { mounts, project, branch, baseUrl, imageRepository };
|
||||||
return { canRender, render };
|
return { canRender, render };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IntegrationsHelpText',
|
||||||
|
components: {
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
GlSprintf,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
messageUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<gl-sprintf :message="message">
|
||||||
|
<template #link="{ content }">
|
||||||
|
<gl-link :href="messageUrl" target="_blank">
|
||||||
|
{{ content }}
|
||||||
|
<gl-icon name="external-link" class="gl-vertical-align-middle" :size="12" />
|
||||||
|
</gl-link>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</span>
|
||||||
|
</template>
|
|
@ -2,7 +2,6 @@
|
||||||
import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
|
import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
|
||||||
import { isSafeURL, joinPaths } from '~/lib/utils/url_utility';
|
import { isSafeURL, joinPaths } from '~/lib/utils/url_utility';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
||||||
import { IMAGE_TABS } from '../../constants';
|
import { IMAGE_TABS } from '../../constants';
|
||||||
import UploadImageTab from './upload_image_tab.vue';
|
import UploadImageTab from './upload_image_tab.vue';
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ export default {
|
||||||
GlTabs,
|
GlTabs,
|
||||||
GlTab,
|
GlTab,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagMixin()],
|
|
||||||
props: {
|
props: {
|
||||||
imageRoot: {
|
imageRoot: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -34,10 +32,10 @@ export default {
|
||||||
},
|
},
|
||||||
modalTitle: __('Image details'),
|
modalTitle: __('Image details'),
|
||||||
okTitle: __('Insert image'),
|
okTitle: __('Insert image'),
|
||||||
urlTabTitle: __('By URL'),
|
urlTabTitle: __('Link to an image'),
|
||||||
urlLabel: __('Image URL'),
|
urlLabel: __('Image URL'),
|
||||||
descriptionLabel: __('Description'),
|
descriptionLabel: __('Description'),
|
||||||
uploadTabTitle: __('Upload file'),
|
uploadTabTitle: __('Upload an image'),
|
||||||
computed: {
|
computed: {
|
||||||
altText() {
|
altText() {
|
||||||
return this.description;
|
return this.description;
|
||||||
|
@ -54,7 +52,7 @@ export default {
|
||||||
this.$refs.modal.show();
|
this.$refs.modal.show();
|
||||||
},
|
},
|
||||||
onOk(event) {
|
onOk(event) {
|
||||||
if (this.glFeatures.sseImageUploads && this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
|
if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
|
||||||
this.submitFile(event);
|
this.submitFile(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +106,7 @@ export default {
|
||||||
:ok-title="$options.okTitle"
|
:ok-title="$options.okTitle"
|
||||||
@ok="onOk"
|
@ok="onOk"
|
||||||
>
|
>
|
||||||
<gl-tabs v-if="glFeatures.sseImageUploads" v-model="tabIndex">
|
<gl-tabs v-model="tabIndex">
|
||||||
<!-- Upload file Tab -->
|
<!-- Upload file Tab -->
|
||||||
<gl-tab :title="$options.uploadTabTitle">
|
<gl-tab :title="$options.uploadTabTitle">
|
||||||
<upload-image-tab ref="uploadImageTab" @input="setFile" />
|
<upload-image-tab ref="uploadImageTab" @input="setFile" />
|
||||||
|
@ -128,17 +126,6 @@ export default {
|
||||||
</gl-tab>
|
</gl-tab>
|
||||||
</gl-tabs>
|
</gl-tabs>
|
||||||
|
|
||||||
<gl-form-group
|
|
||||||
v-else
|
|
||||||
class="gl-mt-5 gl-mb-3"
|
|
||||||
:label="$options.urlLabel"
|
|
||||||
label-for="url-input"
|
|
||||||
:state="!Boolean(urlError)"
|
|
||||||
:invalid-feedback="urlError"
|
|
||||||
>
|
|
||||||
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
|
|
||||||
</gl-form-group>
|
|
||||||
|
|
||||||
<!-- Description Input -->
|
<!-- Description Input -->
|
||||||
<gl-form-group :label="$options.descriptionLabel" label-for="description-input">
|
<gl-form-group :label="$options.descriptionLabel" label-for="description-input">
|
||||||
<gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
|
<gl-form-input id="description-input" ref="descriptionInput" v-model="description" />
|
||||||
|
|
|
@ -114,10 +114,9 @@ export default {
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
this.$emit('uploadImage', { file, imageUrl });
|
this.$emit('uploadImage', { file, imageUrl });
|
||||||
// TODO - ensure that the actual repo URL for the image is used in Markdown mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addImage(this.editorInstance, image);
|
addImage(this.editorInstance, image, file);
|
||||||
},
|
},
|
||||||
onOpenInsertVideoModal() {
|
onOpenInsertVideoModal() {
|
||||||
this.$refs.insertVideoModal.show();
|
this.$refs.insertVideoModal.show();
|
||||||
|
|
|
@ -34,6 +34,20 @@ const buildVideoIframe = src => {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildImg = (alt, originalSrc, file) => {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
const src = file ? URL.createObjectURL(file) : originalSrc;
|
||||||
|
const attributes = { alt, src };
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
img.dataset.originalSrc = originalSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(img, attributes);
|
||||||
|
|
||||||
|
return img;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateToolbarItem = config => {
|
export const generateToolbarItem = config => {
|
||||||
const { icon, classes, event, command, tooltip, isDivider } = config;
|
const { icon, classes, event, command, tooltip, isDivider } = config;
|
||||||
|
|
||||||
|
@ -59,7 +73,14 @@ export const addCustomEventListener = (editorApi, event, handler) => {
|
||||||
export const removeCustomEventListener = (editorApi, event, handler) =>
|
export const removeCustomEventListener = (editorApi, event, handler) =>
|
||||||
editorApi.eventManager.removeEventHandler(event, handler);
|
editorApi.eventManager.removeEventHandler(event, handler);
|
||||||
|
|
||||||
export const addImage = ({ editor }, image) => editor.exec('AddImage', image);
|
export const addImage = ({ editor }, { altText, imageUrl }, file) => {
|
||||||
|
if (editor.isWysiwygMode()) {
|
||||||
|
const img = buildImg(altText, imageUrl, file);
|
||||||
|
editor.getSquire().insertElement(img);
|
||||||
|
} else {
|
||||||
|
editor.insertText(`![${altText}](${imageUrl})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const insertVideo = ({ editor }, url) => {
|
export const insertVideo = ({ editor }, url) => {
|
||||||
const videoIframe = buildVideoIframe(url);
|
const videoIframe = buildVideoIframe(url);
|
||||||
|
|
|
@ -31,6 +31,10 @@ module NotesActions
|
||||||
# We know there's more data, so tell the frontend to poll again after 1ms
|
# We know there's more data, so tell the frontend to poll again after 1ms
|
||||||
set_polling_interval_header(interval: 1) if meta[:more]
|
set_polling_interval_header(interval: 1) if meta[:more]
|
||||||
|
|
||||||
|
# Only present an ETag for the empty response to ensure pagination works
|
||||||
|
# as expected
|
||||||
|
::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present?
|
||||||
|
|
||||||
render json: meta.merge(notes: notes)
|
render json: meta.merge(notes: notes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -115,7 +119,7 @@ module NotesActions
|
||||||
end
|
end
|
||||||
|
|
||||||
def gather_some_notes
|
def gather_some_notes
|
||||||
paginator = Gitlab::UpdatedNotesPaginator.new(
|
paginator = ::Gitlab::UpdatedNotesPaginator.new(
|
||||||
notes_finder.execute.inc_relations_for_view,
|
notes_finder.execute.inc_relations_for_view,
|
||||||
last_fetched_at: last_fetched_at
|
last_fetched_at: last_fetched_at
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
environment: environment,
|
environment: environment,
|
||||||
merge_request: @merge_request,
|
merge_request: @merge_request,
|
||||||
diff_view: diff_view,
|
diff_view: diff_view,
|
||||||
|
merge_ref_head_diff: render_merge_ref_head_diff?,
|
||||||
pagination_data: diffs.pagination_data
|
pagination_data: diffs.pagination_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +68,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
render: ->(partial, locals) { view_to_html_string(partial, locals) }
|
render: ->(partial, locals) { view_to_html_string(partial, locals) }
|
||||||
}
|
}
|
||||||
|
|
||||||
options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project, default_enabled: true) ? "inline" : diff_view)
|
options = additional_attributes.merge(
|
||||||
|
diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project, default_enabled: true) ? "inline" : diff_view,
|
||||||
|
merge_ref_head_diff: render_merge_ref_head_diff?
|
||||||
|
)
|
||||||
|
|
||||||
if @merge_request.project.context_commits_enabled?
|
if @merge_request.project.context_commits_enabled?
|
||||||
options[:context_commits] = @merge_request.recent_context_commits
|
options[:context_commits] = @merge_request.recent_context_commits
|
||||||
|
@ -116,7 +120,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref?
|
if render_merge_ref_head_diff?
|
||||||
return CompareService.new(@project, @merge_request.merge_ref_head.sha)
|
return CompareService.new(@project, @merge_request.merge_ref_head.sha)
|
||||||
.execute(@project, @merge_request.target_branch)
|
.execute(@project, @merge_request.target_branch)
|
||||||
end
|
end
|
||||||
|
@ -158,6 +162,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
|
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_merge_ref_head_diff?
|
||||||
|
Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref?
|
||||||
|
end
|
||||||
|
|
||||||
def note_positions
|
def note_positions
|
||||||
@note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
|
@note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,9 +16,6 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
|
||||||
prepend_before_action :authenticate_user!, only: [:show]
|
prepend_before_action :authenticate_user!, only: [:show]
|
||||||
before_action :assign_ref_and_path, only: [:show]
|
before_action :assign_ref_and_path, only: [:show]
|
||||||
before_action :authorize_edit_tree!, only: [:show]
|
before_action :authorize_edit_tree!, only: [:show]
|
||||||
before_action do
|
|
||||||
push_frontend_feature_flag(:sse_image_uploads)
|
|
||||||
end
|
|
||||||
|
|
||||||
feature_category :static_site_editor
|
feature_category :static_site_editor
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Concern that will eliminate N+1 queries for size-constrained
|
||||||
|
# collections of items.
|
||||||
|
#
|
||||||
|
# **note**: The resolver will never load more items than
|
||||||
|
# `@field.max_page_size` if defined, falling back to
|
||||||
|
# `context.schema.default_max_page_size`.
|
||||||
|
#
|
||||||
|
# provided that:
|
||||||
|
#
|
||||||
|
# - the query can be uniquely determined by the object and the arguments
|
||||||
|
# - the model class includes FromUnion
|
||||||
|
# - the model class defines a scalar primary key
|
||||||
|
#
|
||||||
|
# This comes at the cost of returning arrays, not relations, so we don't get
|
||||||
|
# any keyset pagination goodness. Consequently, this is only suitable for small-ish
|
||||||
|
# result sets, as the full result set will be loaded into memory.
|
||||||
|
#
|
||||||
|
# To enforce this, the resolver limits the size of result sets to
|
||||||
|
# `@field.max_page_size || context.schema.default_max_page_size`.
|
||||||
|
#
|
||||||
|
# **important**: If the cardinality of your collection is likely to be greater than 100,
|
||||||
|
# then you will want to pass `max_page_size:` as part of the field definition
|
||||||
|
# or (ideally) as part of the resolver `field_options`.
|
||||||
|
#
|
||||||
|
# How to implement:
|
||||||
|
# --------------------
|
||||||
|
#
|
||||||
|
# Each including class operates on two generic parameters, A and R:
|
||||||
|
# - A is any Object that can be used as a Hash key. Instances of A
|
||||||
|
# are returned by `query_input` and then passed to `query_for`.
|
||||||
|
# - R is any subclass of ApplicationRecord that includes FromUnion.
|
||||||
|
# R must have a single scalar primary_key
|
||||||
|
#
|
||||||
|
# Classes must implement:
|
||||||
|
# - #model_class -> Class[R]. (Must respond to :primary_key, and :from_union)
|
||||||
|
# - #query_input(**kwargs) -> A (Must be hashable)
|
||||||
|
# - #query_for(A) -> ActiveRecord::Relation[R]
|
||||||
|
#
|
||||||
|
# Note the relationship between query_input and query_for, one of which
|
||||||
|
# consumes the input of the other
|
||||||
|
# (i.e. `resolve(**args).sync == query_for(query_input(**args)).to_a`).
|
||||||
|
#
|
||||||
|
# Classes may implement:
|
||||||
|
# - #item_found(A, R) (return value is ignored)
|
||||||
|
# - max_union_size Integer (the maximum number of queries to run in any one union)
|
||||||
|
module CachingArrayResolver
|
||||||
|
MAX_UNION_SIZE = 50
|
||||||
|
|
||||||
|
def resolve(**args)
|
||||||
|
key = query_input(**args)
|
||||||
|
|
||||||
|
BatchLoader::GraphQL.for(key).batch(**batch) do |keys, loader|
|
||||||
|
if keys.size == 1
|
||||||
|
# We can avoid the union entirely.
|
||||||
|
k = keys.first
|
||||||
|
limit(query_for(k)).each { |item| found(loader, k, item) }
|
||||||
|
else
|
||||||
|
queries = keys.map { |key| query_for(key) }
|
||||||
|
|
||||||
|
queries.in_groups_of(max_union_size, false).each do |group|
|
||||||
|
by_id = model_class
|
||||||
|
.from_union(tag(group), remove_duplicates: false)
|
||||||
|
.group_by { |r| r[primary_key] }
|
||||||
|
|
||||||
|
by_id.values.each do |item_group|
|
||||||
|
item = item_group.first
|
||||||
|
item_group.map(&:union_member_idx).each do |i|
|
||||||
|
found(loader, keys[i], item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override this to intercept the items once they are found
|
||||||
|
def item_found(query_input, item)
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_union_size
|
||||||
|
MAX_UNION_SIZE
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def primary_key
|
||||||
|
@primary_key ||= (model_class.primary_key || raise("No primary key for #{model_class}"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
{ key: self.class, default_value: [] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def found(loader, key, value)
|
||||||
|
loader.call(key) do |vs|
|
||||||
|
item_found(key, value)
|
||||||
|
vs << value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tag each row returned from each query with a the index of which query in
|
||||||
|
# the union it comes from. This lets us map the results back to the cache key.
|
||||||
|
def tag(queries)
|
||||||
|
queries.each_with_index.map do |q, i|
|
||||||
|
limit(q.select(all_fields, member_idx(i)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit(query)
|
||||||
|
query.limit(query_limit) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_fields
|
||||||
|
model_class.arel_table[Arel.star]
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop: disable Graphql/Descriptions (false positive!)
|
||||||
|
def query_limit
|
||||||
|
field&.max_page_size.presence || context.schema.default_max_page_size
|
||||||
|
end
|
||||||
|
# rubocop: enable Graphql/Descriptions
|
||||||
|
|
||||||
|
def member_idx(idx)
|
||||||
|
::Arel::Nodes::SqlLiteral.new(idx.to_s).as('union_member_idx')
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@ module ResolvesSnippets
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
type Types::SnippetType, null: false
|
type Types::SnippetType.connection_type, null: false
|
||||||
|
|
||||||
argument :ids, [::Types::GlobalIDType[::Snippet]],
|
argument :ids, [::Types::GlobalIDType[::Snippet]],
|
||||||
required: false,
|
required: false,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# rubocop:disable Graphql/ResolverType
|
||||||
|
|
||||||
module Resolvers
|
module Resolvers
|
||||||
module Projects
|
module Projects
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# rubocop:disable Graphql/ResolverType
|
||||||
|
|
||||||
module Resolvers
|
module Resolvers
|
||||||
class SnippetsResolver < BaseResolver
|
class SnippetsResolver < BaseResolver
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# rubocop:disable Graphql/ResolverType
|
||||||
|
|
||||||
module Resolvers
|
module Resolvers
|
||||||
module Users
|
module Users
|
||||||
|
|
|
@ -252,4 +252,18 @@ module DiffHelper
|
||||||
|
|
||||||
"...#{path[-(max - 3)..-1]}"
|
"...#{path[-(max - 3)..-1]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def code_navigation_path(diffs)
|
||||||
|
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conflicts
|
||||||
|
return unless options[:merge_ref_head_diff]
|
||||||
|
|
||||||
|
conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request) # rubocop:disable CodeReuse/ServiceClass
|
||||||
|
|
||||||
|
return unless conflicts_service.can_be_resolved_in_ui?
|
||||||
|
|
||||||
|
conflicts_service.conflicts.files.index_by(&:our_path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
module GitpodHelper
|
module GitpodHelper
|
||||||
def gitpod_enable_description
|
def gitpod_enable_description
|
||||||
link_start = '<a href="https://gitpod.io/" target="_blank" rel="noopener noreferrer">'.html_safe
|
s_('Enable %{linkStart}Gitpod%{linkEnd} integration to launch a development environment in your browser directly from GitLab.')
|
||||||
link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
|
|
||||||
|
|
||||||
s_('Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab.').html_safe % { link_start: link_start, link_end: link_end }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -82,8 +82,8 @@ module PreferencesHelper
|
||||||
|
|
||||||
def integration_views
|
def integration_views
|
||||||
[].tap do |views|
|
[].tap do |views|
|
||||||
views << 'gitpod' if Gitlab::Gitpod.feature_and_settings_enabled?
|
views << { name: 'gitpod', message: gitpod_enable_description, message_url: 'https://gitpod.io/', help_link: help_page_path('integration/gitpod.md') } if Gitlab::Gitpod.feature_and_settings_enabled?
|
||||||
views << 'sourcegraph' if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
|
views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences.md', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,6 @@ module ReleasesHelper
|
||||||
project_path: @project.full_path,
|
project_path: @project.full_path,
|
||||||
markdown_preview_path: preview_markdown_path(@project),
|
markdown_preview_path: preview_markdown_path(@project),
|
||||||
markdown_docs_path: help_page_path('user/markdown'),
|
markdown_docs_path: help_page_path('user/markdown'),
|
||||||
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
|
|
||||||
release_assets_docs_path: help_page(anchor: 'release-assets'),
|
release_assets_docs_path: help_page(anchor: 'release-assets'),
|
||||||
manage_milestones_path: project_milestones_path(@project),
|
manage_milestones_path: project_milestones_path(@project),
|
||||||
new_milestone_path: new_project_milestone_path(@project)
|
new_milestone_path: new_project_milestone_path(@project)
|
||||||
|
|
|
@ -2,20 +2,14 @@
|
||||||
|
|
||||||
module SourcegraphHelper
|
module SourcegraphHelper
|
||||||
def sourcegraph_url_message
|
def sourcegraph_url_message
|
||||||
link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: Gitlab::CurrentSettings.sourcegraph_url }
|
|
||||||
link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
|
|
||||||
|
|
||||||
message =
|
message =
|
||||||
if Gitlab::CurrentSettings.sourcegraph_url_is_com?
|
if Gitlab::CurrentSettings.sourcegraph_url_is_com?
|
||||||
s_('SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}.').html_safe
|
s_('SourcegraphPreferences|Uses %{linkStart}Sourcegraph.com%{linkEnd}.').html_safe
|
||||||
else
|
else
|
||||||
s_('SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}.').html_safe
|
s_('SourcegraphPreferences|Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}.').html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
message % { link_start: link_start, link_end: link_end }
|
experimental_message =
|
||||||
end
|
|
||||||
|
|
||||||
def sourcegraph_experimental_message
|
|
||||||
if Gitlab::Sourcegraph.feature_conditional?
|
if Gitlab::Sourcegraph.feature_conditional?
|
||||||
s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.")
|
s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.")
|
||||||
elsif Gitlab::CurrentSettings.sourcegraph_public_only
|
elsif Gitlab::CurrentSettings.sourcegraph_public_only
|
||||||
|
@ -23,5 +17,7 @@ module SourcegraphHelper
|
||||||
else
|
else
|
||||||
s_("SourcegraphPreferences|This feature is experimental.")
|
s_("SourcegraphPreferences|This feature is experimental.")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"#{message} #{experimental_message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class DiffFileEntity < DiffFileBaseEntity
|
class DiffFileEntity < DiffFileBaseEntity
|
||||||
include CommitsHelper
|
include CommitsHelper
|
||||||
include IconsHelper
|
include IconsHelper
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
expose :added_lines
|
expose :added_lines
|
||||||
expose :removed_lines
|
expose :removed_lines
|
||||||
|
@ -54,12 +55,17 @@ class DiffFileEntity < DiffFileBaseEntity
|
||||||
|
|
||||||
# Used for inline diffs
|
# Used for inline diffs
|
||||||
expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { inline_diff_view?(options, diff_file) && diff_file.text? } do |diff_file|
|
expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { inline_diff_view?(options, diff_file) && diff_file.text? } do |diff_file|
|
||||||
diff_file.diff_lines_for_serializer
|
file = conflict_file(options, diff_file) || diff_file
|
||||||
|
file.diff_lines_for_serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :is_fully_expanded do |diff_file|
|
expose :is_fully_expanded do |diff_file|
|
||||||
|
if conflict_file(options, diff_file)
|
||||||
|
false
|
||||||
|
else
|
||||||
diff_file.fully_expanded?
|
diff_file.fully_expanded?
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Used for parallel diffs
|
# Used for parallel diffs
|
||||||
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, options) { parallel_diff_view?(options, diff_file) && diff_file.text? }
|
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, options) { parallel_diff_view?(options, diff_file) && diff_file.text? }
|
||||||
|
@ -79,4 +85,10 @@ class DiffFileEntity < DiffFileBaseEntity
|
||||||
# If nothing is present, inline will be the default.
|
# If nothing is present, inline will be the default.
|
||||||
options.fetch(:diff_view, :inline).to_sym == :inline
|
options.fetch(:diff_view, :inline).to_sym == :inline
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conflict_file(options, diff_file)
|
||||||
|
strong_memoize(:conflict_file) do
|
||||||
|
options[:conflicts] && options[:conflicts][diff_file.new_path]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,7 +71,7 @@ class DiffsEntity < Grape::Entity
|
||||||
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
|
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
|
||||||
|
|
||||||
DiffFileEntity.represent(diffs.diff_files,
|
DiffFileEntity.represent(diffs.diff_files,
|
||||||
options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs)))
|
options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs), conflicts: conflicts))
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
|
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
|
||||||
|
@ -88,10 +88,6 @@ class DiffsEntity < Grape::Entity
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def code_navigation_path(diffs)
|
|
||||||
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
|
|
||||||
end
|
|
||||||
|
|
||||||
def commit_ids
|
def commit_ids
|
||||||
@commit_ids ||= merge_request.recent_commits.map(&:id)
|
@commit_ids ||= merge_request.recent_commits.map(&:id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#
|
#
|
||||||
class PaginatedDiffEntity < Grape::Entity
|
class PaginatedDiffEntity < Grape::Entity
|
||||||
include RequestAwareEntity
|
include RequestAwareEntity
|
||||||
|
include DiffHelper
|
||||||
|
|
||||||
expose :diff_files do |diffs, options|
|
expose :diff_files do |diffs, options|
|
||||||
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
|
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
|
||||||
|
@ -15,7 +16,8 @@ class PaginatedDiffEntity < Grape::Entity
|
||||||
diffs.diff_files,
|
diffs.diff_files,
|
||||||
options.merge(
|
options.merge(
|
||||||
submodule_links: submodule_links,
|
submodule_links: submodule_links,
|
||||||
code_navigation_path: code_navigation_path(diffs)
|
code_navigation_path: code_navigation_path(diffs),
|
||||||
|
conflicts: conflicts
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -41,10 +43,6 @@ class PaginatedDiffEntity < Grape::Entity
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def code_navigation_path(diffs)
|
|
||||||
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
|
|
||||||
end
|
|
||||||
|
|
||||||
%i[current_page next_page total_pages].each do |method|
|
%i[current_page next_page total_pages].each do |method|
|
||||||
define_method method do
|
define_method method do
|
||||||
pagination_data[method]
|
pagination_data[method]
|
||||||
|
|
|
@ -17,6 +17,7 @@ module Users
|
||||||
user.accept_pending_invitations! if user.active_for_authentication?
|
user.accept_pending_invitations! if user.active_for_authentication?
|
||||||
DeviseMailer.user_admin_approval(user).deliver_later
|
DeviseMailer.user_admin_approval(user).deliver_later
|
||||||
|
|
||||||
|
after_approve_hook(user)
|
||||||
success
|
success
|
||||||
else
|
else
|
||||||
error(user.errors.full_messages.uniq.join('. '))
|
error(user.errors.full_messages.uniq.join('. '))
|
||||||
|
@ -27,6 +28,10 @@ module Users
|
||||||
|
|
||||||
attr_reader :current_user
|
attr_reader :current_user
|
||||||
|
|
||||||
|
def after_approve_hook(user)
|
||||||
|
# overridden by EE module
|
||||||
|
end
|
||||||
|
|
||||||
def allowed?
|
def allowed?
|
||||||
can?(current_user, :approve_user)
|
can?(current_user, :approve_user)
|
||||||
end
|
end
|
||||||
|
@ -36,3 +41,5 @@ module Users
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Users::ApproveService.prepend_if_ee('EE::Users::ApproveService')
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
%p
|
%p
|
||||||
= gitpod_enable_description
|
%integration-help-text{ "id" => "js-gitpod-settings-help-text", "message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" }
|
||||||
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,9 @@
|
||||||
- add_page_specific_style 'page_bundles/signup'
|
- add_page_specific_style 'page_bundles/signup'
|
||||||
|
|
||||||
.signup-page
|
.signup-page
|
||||||
= render 'devise/shared/signup_box', url: registration_path(resource_name), button_text: _('Register'), show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?
|
= render 'devise/shared/signup_box',
|
||||||
|
url: registration_path(resource_name),
|
||||||
|
button_text: _('Register'),
|
||||||
|
show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?,
|
||||||
|
suggestion_path: nil
|
||||||
= render 'devise/shared/sign_in_link'
|
= render 'devise/shared/sign_in_link'
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
%label.label-bold#gitpod
|
|
||||||
= s_('Gitpod')
|
|
||||||
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
|
||||||
.form-group.form-check
|
|
||||||
= f.check_box :gitpod_enabled, class: 'form-check-input'
|
|
||||||
= f.label :gitpod_enabled, class: 'form-check-label' do
|
|
||||||
= s_('Gitpod|Enable Gitpod integration').html_safe
|
|
||||||
.form-text.text-muted
|
|
||||||
= gitpod_enable_description
|
|
|
@ -1,18 +0,0 @@
|
||||||
- views = integration_views
|
|
||||||
- return unless views.any?
|
|
||||||
|
|
||||||
.col-sm-12
|
|
||||||
%hr
|
|
||||||
|
|
||||||
.col-lg-4.profile-settings-sidebar#integrations
|
|
||||||
%h4.gl-mt-0
|
|
||||||
= s_('Preferences|Integrations')
|
|
||||||
%p
|
|
||||||
= s_('Preferences|Customize integrations with third party services.')
|
|
||||||
= succeed '.' do
|
|
||||||
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
|
|
||||||
|
|
||||||
.col-lg-8
|
|
||||||
- views.each do |view|
|
|
||||||
= render view, f: f
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
%label.label-bold
|
|
||||||
= s_('Preferences|Sourcegraph')
|
|
||||||
= link_to sprite_icon('question-o'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
|
||||||
.form-group.form-check
|
|
||||||
= f.check_box :sourcegraph_enabled, class: 'form-check-input'
|
|
||||||
= f.label :sourcegraph_enabled, class: 'form-check-label' do
|
|
||||||
= s_('Preferences|Enable integrated code intelligence on code views').html_safe
|
|
||||||
.form-text.text-muted
|
|
||||||
= sourcegraph_url_message
|
|
||||||
= sourcegraph_experimental_message
|
|
|
@ -1,10 +1,14 @@
|
||||||
- page_title _('Preferences')
|
- page_title _('Preferences')
|
||||||
- @content_class = "limit-container-width" unless fluid_layout
|
- @content_class = "limit-container-width" unless fluid_layout
|
||||||
|
- user_fields = { gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }
|
||||||
|
- user_theme_id = Gitlab::Themes.for_user(@user).id
|
||||||
|
- data_attributes = { integration_views: integration_views.to_json, user_fields: user_fields.to_json }
|
||||||
|
|
||||||
- Gitlab::Themes.each do |theme|
|
- Gitlab::Themes.each do |theme|
|
||||||
= stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename
|
= stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename
|
||||||
|
|
||||||
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row gl-mt-3 js-preferences-form' } do |f|
|
= form_for @user, url: profile_preferences_path, remote: true, method: :put do |f|
|
||||||
|
.row.gl-mt-3.js-preferences-form
|
||||||
.col-lg-4.application-theme#navigation-theme
|
.col-lg-4.application-theme#navigation-theme
|
||||||
%h4.gl-mt-0
|
%h4.gl-mt-0
|
||||||
= s_('Preferences|Navigation theme')
|
= s_('Preferences|Navigation theme')
|
||||||
|
@ -15,7 +19,7 @@
|
||||||
- Gitlab::Themes.each do |theme|
|
- Gitlab::Themes.each do |theme|
|
||||||
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
|
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
|
||||||
.preview{ class: theme.css_class }
|
.preview{ class: theme.css_class }
|
||||||
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
|
= f.radio_button :theme_id, theme.id, checked: user_theme_id == theme.id
|
||||||
= theme.name
|
= theme.name
|
||||||
|
|
||||||
.col-sm-12
|
.col-sm-12
|
||||||
|
@ -138,8 +142,9 @@
|
||||||
.form-text.text-muted
|
.form-text.text-muted
|
||||||
= s_('Preferences|For example: 30 mins ago.')
|
= s_('Preferences|For example: 30 mins ago.')
|
||||||
|
|
||||||
= render 'integrations', f: f
|
#js-profile-preferences-app{ data: data_attributes, user_fields: user_fields.to_json }
|
||||||
|
|
||||||
|
.row.gl-mt-3.js-preferences-form
|
||||||
.col-lg-4.profile-settings-sidebar
|
.col-lg-4.profile-settings-sidebar
|
||||||
.col-lg-8
|
.col-lg-8
|
||||||
.form-group
|
.form-group
|
||||||
|
|
|
@ -20,7 +20,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
|
||||||
changes = Base64.decode64(changes) unless changes.include?(' ')
|
changes = Base64.decode64(changes) unless changes.include?(' ')
|
||||||
# Use Sidekiq.logger so arguments can be correlated with execution
|
# Use Sidekiq.logger so arguments can be correlated with execution
|
||||||
# time and thread ID's.
|
# time and thread ID's.
|
||||||
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
|
Sidekiq.logger.info "changes: #{changes.inspect}" if SidekiqLogArguments.enabled?
|
||||||
post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
|
post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
|
||||||
|
|
||||||
if repo_type.wiki?
|
if repo_type.wiki?
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Only set an ETag for the notes endpoint after all notes have been sent
|
||||||
|
merge_request: 46810
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Enable the ability to upload images via the SSE
|
||||||
|
merge_request: 36299
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add type annotation for snippet resolvers
|
||||||
|
merge_request: 47548
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update the tag name field helper text on the Edit Release page
|
||||||
|
merge_request: 47234
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Enable Sidekiq argument logging by default
|
||||||
|
merge_request: 44853
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
name: display_merge_conflicts_in_diff
|
name: display_merge_conflicts_in_diff
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45008
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45008
|
||||||
rollout_issue_url:
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/277097
|
||||||
milestone: '13.5'
|
milestone: '13.5'
|
||||||
type: development
|
type: development
|
||||||
group: group::source code
|
group: group::source code
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
module SidekiqLogArguments
|
||||||
|
def self.enabled?
|
||||||
|
Gitlab::Utils.to_boolean(ENV['SIDEKIQ_LOG_ARGUMENTS'], default: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def enable_reliable_fetch?
|
def enable_reliable_fetch?
|
||||||
return true unless Feature::FlipperFeature.table_exists?
|
return true unless Feature::FlipperFeature.table_exists?
|
||||||
|
@ -35,7 +40,7 @@ Sidekiq.configure_server do |config|
|
||||||
|
|
||||||
config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
|
config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
|
||||||
metrics: Settings.monitoring.sidekiq_exporter,
|
metrics: Settings.monitoring.sidekiq_exporter,
|
||||||
arguments_logger: ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs,
|
arguments_logger: SidekiqLogArguments.enabled? && !enable_json_logs,
|
||||||
memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer
|
memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ From there, you can see the following actions:
|
||||||
- Number of required approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9)
|
- Number of required approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9)
|
||||||
- Added or removed users and groups from project approval groups ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213603) in GitLab 13.2)
|
- Added or removed users and groups from project approval groups ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213603) in GitLab 13.2)
|
||||||
- Project CI/CD variable added, removed, or protected status changed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.4)
|
- Project CI/CD variable added, removed, or protected status changed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.4)
|
||||||
|
- User was approved via Admin Area ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276250) in GitLab 13.6)
|
||||||
|
|
||||||
Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events).
|
Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events).
|
||||||
|
|
||||||
|
|
|
@ -856,7 +856,9 @@ This file is stored in:
|
||||||
- `/var/log/gitlab/gitlab-rails/update_mirror_service_json.log` for Omnibus GitLab installations.
|
- `/var/log/gitlab/gitlab-rails/update_mirror_service_json.log` for Omnibus GitLab installations.
|
||||||
- `/home/git/gitlab/log/update_mirror_service_json.log` for installations from source.
|
- `/home/git/gitlab/log/update_mirror_service_json.log` for installations from source.
|
||||||
|
|
||||||
This file contains information about any errors that occurred during project mirroring.
|
This file contains information about LFS errors that occurred during project mirroring.
|
||||||
|
While we work to move other project mirroring errors into this log, the [general log](#productionlog)
|
||||||
|
can be used.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,19 +26,11 @@ preventing other threads from continuing.
|
||||||
|
|
||||||
## Log arguments to Sidekiq jobs
|
## Log arguments to Sidekiq jobs
|
||||||
|
|
||||||
If you want to see what arguments are being passed to Sidekiq jobs you can set
|
[In GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44853)
|
||||||
the `SIDEKIQ_LOG_ARGUMENTS` [environment variable](https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true).
|
some arguments passed to Sidekiq jobs are logged by default.
|
||||||
|
To avoid logging sensitive information (for instance, password reset tokens),
|
||||||
Example:
|
GitLab logs numeric arguments for all workers, with overrides for some specific
|
||||||
|
workers where their arguments are not sensitive.
|
||||||
```ruby
|
|
||||||
gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"}
|
|
||||||
```
|
|
||||||
|
|
||||||
This does not log all job arguments. To avoid logging sensitive
|
|
||||||
information (for instance, password reset tokens), it logs numeric
|
|
||||||
arguments for all workers, with overrides for some specific workers
|
|
||||||
where their arguments are not sensitive.
|
|
||||||
|
|
||||||
Example log output:
|
Example log output:
|
||||||
|
|
||||||
|
@ -53,6 +45,17 @@ arguments logs are limited to a maximum size of 10 kilobytes of text;
|
||||||
any arguments after this limit will be discarded and replaced with a
|
any arguments after this limit will be discarded and replaced with a
|
||||||
single argument containing the string `"..."`.
|
single argument containing the string `"..."`.
|
||||||
|
|
||||||
|
You can set `SIDEKIQ_LOG_ARGUMENTS` [environment variable](https://docs.gitlab.com/omnibus/settings/environment-variables.html)
|
||||||
|
to `0` (false) to disable argument logging.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "0"}
|
||||||
|
```
|
||||||
|
|
||||||
|
In GitLab 13.5 and earlier, set `SIDEKIQ_LOG_ARGUMENTS` to `1` to start logging arguments passed to Sidekiq.
|
||||||
|
|
||||||
## Thread dump
|
## Thread dump
|
||||||
|
|
||||||
Send the Sidekiq process ID the `TTIN` signal and it will output thread
|
Send the Sidekiq process ID the `TTIN` signal and it will output thread
|
||||||
|
|
|
@ -1904,6 +1904,16 @@ input BoardIssueInput {
|
||||||
"""
|
"""
|
||||||
epicWildcardId: EpicWildcardId
|
epicWildcardId: EpicWildcardId
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter by iteration title
|
||||||
|
"""
|
||||||
|
iterationTitle: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter by iteration ID wildcard
|
||||||
|
"""
|
||||||
|
iterationWildcardId: IterationWildcardId
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Filter by label name
|
Filter by label name
|
||||||
"""
|
"""
|
||||||
|
@ -11239,11 +11249,6 @@ enum IssueType {
|
||||||
Represents an iteration object
|
Represents an iteration object
|
||||||
"""
|
"""
|
||||||
type Iteration implements TimeboxReportInterface {
|
type Iteration implements TimeboxReportInterface {
|
||||||
"""
|
|
||||||
Daily scope and completed totals for burnup charts
|
|
||||||
"""
|
|
||||||
burnupTimeSeries: [BurnupChartDailyTotals!]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Timestamp of iteration creation
|
Timestamp of iteration creation
|
||||||
"""
|
"""
|
||||||
|
@ -11371,6 +11376,21 @@ enum IterationState {
|
||||||
upcoming
|
upcoming
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Iteration ID wildcard values
|
||||||
|
"""
|
||||||
|
enum IterationWildcardId {
|
||||||
|
"""
|
||||||
|
An iteration is assigned
|
||||||
|
"""
|
||||||
|
ANY
|
||||||
|
|
||||||
|
"""
|
||||||
|
No iteration is assigned
|
||||||
|
"""
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Represents untyped JSON
|
Represents untyped JSON
|
||||||
"""
|
"""
|
||||||
|
@ -13314,11 +13334,6 @@ type MetricsDashboardAnnotationEdge {
|
||||||
Represents a milestone
|
Represents a milestone
|
||||||
"""
|
"""
|
||||||
type Milestone implements TimeboxReportInterface {
|
type Milestone implements TimeboxReportInterface {
|
||||||
"""
|
|
||||||
Daily scope and completed totals for burnup charts
|
|
||||||
"""
|
|
||||||
burnupTimeSeries: [BurnupChartDailyTotals!]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Timestamp of milestone creation
|
Timestamp of milestone creation
|
||||||
"""
|
"""
|
||||||
|
@ -13883,6 +13898,11 @@ input NegatedBoardIssueInput {
|
||||||
"""
|
"""
|
||||||
epicId: EpicID
|
epicId: EpicID
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter by iteration title
|
||||||
|
"""
|
||||||
|
iterationTitle: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Filter by label name
|
Filter by label name
|
||||||
"""
|
"""
|
||||||
|
@ -20948,11 +20968,6 @@ type TimeboxReport {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeboxReportInterface {
|
interface TimeboxReportInterface {
|
||||||
"""
|
|
||||||
Daily scope and completed totals for burnup charts
|
|
||||||
"""
|
|
||||||
burnupTimeSeries: [BurnupChartDailyTotals!]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Historically accurate report about the timebox
|
Historically accurate report about the timebox
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5111,6 +5111,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "iterationTitle",
|
||||||
|
"description": "Filter by iteration title",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "weight",
|
"name": "weight",
|
||||||
"description": "Filter by weight",
|
"description": "Filter by weight",
|
||||||
|
@ -5150,6 +5160,16 @@
|
||||||
"ofType": null
|
"ofType": null
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "iterationWildcardId",
|
||||||
|
"description": "Filter by iteration ID wildcard",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "IterationWildcardId",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"interfaces": null,
|
"interfaces": null,
|
||||||
|
@ -30696,28 +30716,6 @@
|
||||||
"name": "Iteration",
|
"name": "Iteration",
|
||||||
"description": "Represents an iteration object",
|
"description": "Represents an iteration object",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"name": "burnupTimeSeries",
|
|
||||||
"description": "Daily scope and completed totals for burnup charts",
|
|
||||||
"args": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"type": {
|
|
||||||
"kind": "LIST",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "OBJECT",
|
|
||||||
"name": "BurnupChartDailyTotals",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"description": "Timestamp of iteration creation",
|
"description": "Timestamp of iteration creation",
|
||||||
|
@ -31135,6 +31133,29 @@
|
||||||
],
|
],
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "IterationWildcardId",
|
||||||
|
"description": "Iteration ID wildcard values",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "NONE",
|
||||||
|
"description": "No iteration is assigned",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ANY",
|
||||||
|
"description": "An iteration is assigned",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "JSON",
|
"name": "JSON",
|
||||||
|
@ -36633,28 +36654,6 @@
|
||||||
"name": "Milestone",
|
"name": "Milestone",
|
||||||
"description": "Represents a milestone",
|
"description": "Represents a milestone",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"name": "burnupTimeSeries",
|
|
||||||
"description": "Daily scope and completed totals for burnup charts",
|
|
||||||
"args": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"type": {
|
|
||||||
"kind": "LIST",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "OBJECT",
|
|
||||||
"name": "BurnupChartDailyTotals",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"description": "Timestamp of milestone creation",
|
"description": "Timestamp of milestone creation",
|
||||||
|
@ -41116,6 +41115,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "iterationTitle",
|
||||||
|
"description": "Filter by iteration title",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "weight",
|
"name": "weight",
|
||||||
"description": "Filter by weight",
|
"description": "Filter by weight",
|
||||||
|
@ -60920,28 +60929,6 @@
|
||||||
"name": "TimeboxReportInterface",
|
"name": "TimeboxReportInterface",
|
||||||
"description": null,
|
"description": null,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"name": "burnupTimeSeries",
|
|
||||||
"description": "Daily scope and completed totals for burnup charts",
|
|
||||||
"args": [
|
|
||||||
|
|
||||||
],
|
|
||||||
"type": {
|
|
||||||
"kind": "LIST",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "OBJECT",
|
|
||||||
"name": "BurnupChartDailyTotals",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "report",
|
"name": "report",
|
||||||
"description": "Historically accurate report about the timebox",
|
"description": "Historically accurate report about the timebox",
|
||||||
|
|
|
@ -1719,7 +1719,6 @@ Represents an iteration object.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ----- | ---- | ----------- |
|
| ----- | ---- | ----------- |
|
||||||
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
|
|
||||||
| `createdAt` | Time! | Timestamp of iteration creation |
|
| `createdAt` | Time! | Timestamp of iteration creation |
|
||||||
| `description` | String | Description of the iteration |
|
| `description` | String | Description of the iteration |
|
||||||
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
|
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
|
||||||
|
@ -2042,7 +2041,6 @@ Represents a milestone.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ----- | ---- | ----------- |
|
| ----- | ---- | ----------- |
|
||||||
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
|
|
||||||
| `createdAt` | Time! | Timestamp of milestone creation |
|
| `createdAt` | Time! | Timestamp of milestone creation |
|
||||||
| `description` | String | Description of the milestone |
|
| `description` | String | Description of the milestone |
|
||||||
| `dueDate` | Time | Timestamp of the milestone due date |
|
| `dueDate` | Time | Timestamp of the milestone due date |
|
||||||
|
@ -3994,6 +3992,15 @@ State of a GitLab iteration.
|
||||||
| `started` | |
|
| `started` | |
|
||||||
| `upcoming` | |
|
| `upcoming` | |
|
||||||
|
|
||||||
|
### IterationWildcardId
|
||||||
|
|
||||||
|
Iteration ID wildcard values.
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| `ANY` | An iteration is assigned |
|
||||||
|
| `NONE` | No iteration is assigned |
|
||||||
|
|
||||||
### ListLimitMetric
|
### ListLimitMetric
|
||||||
|
|
||||||
List limit metric setting.
|
List limit metric setting.
|
||||||
|
|
|
@ -1927,6 +1927,38 @@ The returned `url` is relative to the project path. The returned `full_path` is
|
||||||
the absolute path to the file. In Markdown contexts, the link is expanded when
|
the absolute path to the file. In Markdown contexts, the link is expanded when
|
||||||
the format in `markdown` is used.
|
the format in `markdown` is used.
|
||||||
|
|
||||||
|
## Upload a project avatar
|
||||||
|
|
||||||
|
Uploads an avatar to the specified project.
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
PUT /projects/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
|-----------|----------------|------------------------|-------------|
|
||||||
|
| `avatar` | string | **{check-circle}** Yes | The file to be uploaded. |
|
||||||
|
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
||||||
|
|
||||||
|
To upload an avatar from your file system, use the `--form` argument. This causes
|
||||||
|
cURL to post data using the header `Content-Type: multipart/form-data`. The
|
||||||
|
`file=` parameter must point to an image file on your file system and be
|
||||||
|
preceded by `@`. For example:
|
||||||
|
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "avatar=@dk.png" "https://gitlab.example.com/api/v4/projects/5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Returned object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"avatar_url": "https://gitlab.example.com/uploads/-/system/project/avatar/2/dk.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Share project with group
|
## Share project with group
|
||||||
|
|
||||||
Allow to share project with group.
|
Allow to share project with group.
|
||||||
|
|
|
@ -284,11 +284,14 @@ When running your project pipeline at this point:
|
||||||
|
|
||||||
#### Custom build job for Auto DevOps
|
#### Custom build job for Auto DevOps
|
||||||
|
|
||||||
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6.
|
||||||
AWS EC2, you must specify a job for the `build` stage.
|
|
||||||
|
|
||||||
To do so, you must reference the `Auto-DevOps.gitlab-ci.yml` template and include a job named
|
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
|
||||||
`build_artifact` in your `.gitlab-ci.yml` file. For example:
|
AWS EC2, first you must define [your AWS credentials as environment variables](#run-aws-commands-from-gitlab-cicd).
|
||||||
|
|
||||||
|
Next, define a job for the `build` stage. To do so, you must reference the
|
||||||
|
`Auto-DevOps.gitlab-ci.yml` template and include a job named `build_artifact` in your
|
||||||
|
`.gitlab-ci.yml` file. For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .gitlab-ci.yml
|
# .gitlab-ci.yml
|
||||||
|
|
|
@ -447,6 +447,23 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
||||||
- `end_date`: end date of the period for which we want to get event data.
|
- `end_date`: end date of the period for which we want to get event data.
|
||||||
- `context`: context of the event. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate`.
|
- `context`: context of the event. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate`.
|
||||||
|
|
||||||
|
1. Testing tracking and getting unique events
|
||||||
|
|
||||||
|
Trigger events in rails console by using `track_event` method
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(1, 'g_compliance_audit_events')
|
||||||
|
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(2, 'g_compliance_audit_events')
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, get the unique events for the current week.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Get unique events for metric for current_week
|
||||||
|
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_audit_events',
|
||||||
|
start_date: Date.current.beginning_of_week, end_date: Date.current.end_of_week)
|
||||||
|
```
|
||||||
|
|
||||||
Recommendations:
|
Recommendations:
|
||||||
|
|
||||||
- Key should expire in 29 days for daily and 42 days for weekly.
|
- Key should expire in 29 days for daily and 42 days for weekly.
|
||||||
|
|
|
@ -696,8 +696,8 @@ blocks:
|
||||||
|
|
||||||
## Arguments logging
|
## Arguments logging
|
||||||
|
|
||||||
When [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs)
|
As of GitLab 13.6, Sidekiq job arguments will be logged by default, unless [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs)
|
||||||
is enabled, Sidekiq job arguments will be logged.
|
is disabled.
|
||||||
|
|
||||||
By default, the only arguments logged are numeric arguments, because
|
By default, the only arguments logged are numeric arguments, because
|
||||||
arguments of other types could contain sensitive information. To
|
arguments of other types could contain sensitive information. To
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB |
|
@ -5,13 +5,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference, concepts
|
type: reference, concepts
|
||||||
---
|
---
|
||||||
|
|
||||||
# Instance-level merge request approval rules **(PREMIUM ONLY)**
|
# Merge request approval rules **(PREMIUM ONLY)**
|
||||||
|
|
||||||
> Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8.
|
> Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8.
|
||||||
|
|
||||||
Merge request approvals rules prevent users overriding certain settings on a project
|
Merge request approval rules prevent users from overriding certain settings on the project
|
||||||
level. When configured, only administrators can change these settings on a project level
|
level. When enabled at the instance level, these settings are no longer editable on the
|
||||||
if they are enabled at an instance level.
|
project level.
|
||||||
|
|
||||||
To enable merge request approval rules for an instance:
|
To enable merge request approval rules for an instance:
|
||||||
|
|
||||||
|
@ -20,8 +20,6 @@ To enable merge request approval rules for an instance:
|
||||||
1. Set the required rule.
|
1. Set the required rule.
|
||||||
1. Click **Save changes**.
|
1. Click **Save changes**.
|
||||||
|
|
||||||
GitLab administrators can later override these settings in a project’s settings.
|
|
||||||
|
|
||||||
## Available rules
|
## Available rules
|
||||||
|
|
||||||
Merge request approval rules that can be set at an instance level are:
|
Merge request approval rules that can be set at an instance level are:
|
||||||
|
@ -31,24 +29,5 @@ Merge request approval rules that can be set at an instance level are:
|
||||||
- **Prevent approval of merge requests by merge request committers**. Prevents project
|
- **Prevent approval of merge requests by merge request committers**. Prevents project
|
||||||
maintainers from allowing users to approve merge requests if they have submitted
|
maintainers from allowing users to approve merge requests if they have submitted
|
||||||
any commits to the source branch.
|
any commits to the source branch.
|
||||||
- **Can override approvers and approvals required per merge request**. Allows project
|
- **Prevent users from modifying merge request approvers list**. Prevents users from
|
||||||
maintainers to modify the approvers list in individual merge requests.
|
modifying the approvers list in project settings or in individual merge requests.
|
||||||
|
|
||||||
## Scope rules to compliance-labeled projects
|
|
||||||
|
|
||||||
> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2.
|
|
||||||
|
|
||||||
Merge request approval rules can be further scoped to specific compliance frameworks.
|
|
||||||
|
|
||||||
When the compliance framework label is selected and the project is assigned the compliance
|
|
||||||
label, the instance-level MR approval settings will take effect and the
|
|
||||||
[project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
|
|
||||||
is locked for modification.
|
|
||||||
|
|
||||||
When the compliance framework label is not selected or the project is not assigned the
|
|
||||||
compliance label, the project-level MR approval settings will take effect and the users with
|
|
||||||
Maintainer role and above can modify these.
|
|
||||||
|
|
||||||
| Instance-level | Project-level |
|
|
||||||
| -------------- | ------------- |
|
|
||||||
| ![Scope MR approval settings to compliance frameworks](img/scope_mr_approval_settings_v13_5.png) | ![MR approval settings on compliance projects](img/mr_approval_settings_compliance_project_v13_5.png) |
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ project to make sure it complies with the separation of duties described above.
|
||||||
The Chain of Custody report allows customers to export a list of merge commits within the group.
|
The Chain of Custody report allows customers to export a list of merge commits within the group.
|
||||||
The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA,
|
The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA,
|
||||||
merge request author, merge request ID, merge user, pipeline ID, group name, project name, and merge request approvers.
|
merge request author, merge request ID, merge user, pipeline ID, group name, project name, and merge request approvers.
|
||||||
|
Depending on the merge strategy, the merge commit SHA can either be a merge commit, squash commit or a diff head commit.
|
||||||
|
|
||||||
To download the Chain of Custody report, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu and click **List of all merge commits**
|
To download the Chain of Custody report, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu and click **List of all merge commits**
|
||||||
|
|
||||||
|
|
|
@ -432,7 +432,7 @@ and the following environment variables:
|
||||||
| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` |
|
| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` |
|
||||||
| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` |
|
| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` |
|
||||||
| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` |
|
| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` |
|
||||||
| `SIDEKIQ_LOG_ARGUMENTS` | `1` | - |
|
| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` |
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import
|
The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import
|
||||||
|
|
|
@ -240,6 +240,14 @@ For users without permissions to view the project's code:
|
||||||
- The wiki homepage is displayed, if any.
|
- The wiki homepage is displayed, if any.
|
||||||
- The list of issues within the project is displayed.
|
- The list of issues within the project is displayed.
|
||||||
|
|
||||||
|
## GitLab Workflow - VS Code extension
|
||||||
|
|
||||||
|
To avoid switching from the GitLab UI and VS Code while working in GitLab repositories, you can integrate
|
||||||
|
the [VS Code](https://code.visualstudio.com/) editor with GitLab through the
|
||||||
|
[GitLab Workflow extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow).
|
||||||
|
|
||||||
|
To review or contribute to the extension's code, visit [its codebase in GitLab](https://gitlab.com/gitlab-org/gitlab-vscode-extension/).
|
||||||
|
|
||||||
## Redirects when changing repository paths
|
## Redirects when changing repository paths
|
||||||
|
|
||||||
When a repository path changes, it is essential to smoothly transition from the
|
When a repository path changes, it is essential to smoothly transition from the
|
||||||
|
|
|
@ -107,13 +107,36 @@ The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing t
|
||||||
### Images
|
### Images
|
||||||
|
|
||||||
> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1.
|
> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1.
|
||||||
|
> - Support for uploading images via the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218529) in GitLab 13.6.
|
||||||
|
|
||||||
You can add image files on the WYSIWYG mode by clicking the image icon (**{doc-image}**).
|
#### Upload an image
|
||||||
From there, link to a URL, add optional [ALT text](https://moz.com/learn/seo/alt-text),
|
|
||||||
and you're done. The link can reference images already hosted in your project, an asset hosted
|
You can upload image files via the WYSIWYG editor directly to the repository to default upload directory
|
||||||
|
`source/images`. To do so:
|
||||||
|
|
||||||
|
1. Click the image icon (**{doc-image}**).
|
||||||
|
1. Choose the **Upload file** tab.
|
||||||
|
1. Click **Choose file** to select a file from your computer.
|
||||||
|
1. Optional: add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)).
|
||||||
|
1. Click **Insert image**.
|
||||||
|
|
||||||
|
The selected file can be any supported image file (`.png`, `.jpg`, `.jpeg`, `.gif`). The editor renders
|
||||||
|
thumbnail previews so you can verify the correct image is included and there aren't any references to
|
||||||
|
missing images.
|
||||||
|
|
||||||
|
#### Link to an image
|
||||||
|
|
||||||
|
You can also link to an image if you'd like:
|
||||||
|
|
||||||
|
1. Click the image icon (**{doc-image}**).
|
||||||
|
1. Choose the **Link to an image** tab.
|
||||||
|
1. Add the link to the image into the **Image URL** field (use the full path; relative paths are not supported yet).
|
||||||
|
1. Optional: add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)).
|
||||||
|
1. Click **Insert image**.
|
||||||
|
|
||||||
|
The link can reference images already hosted in your project, an asset hosted
|
||||||
externally on a content delivery network, or any other external URL. The editor renders thumbnail previews
|
externally on a content delivery network, or any other external URL. The editor renders thumbnail previews
|
||||||
so you can verify the correct image is included and there aren't any references to missing images.
|
so you can verify the correct image is included and there aren't any references to missing images.
|
||||||
default directory (`source/images/`).
|
|
||||||
|
|
||||||
### Videos
|
### Videos
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@ module Gitlab
|
||||||
|
|
||||||
CONTEXT_LINES = 3
|
CONTEXT_LINES = 3
|
||||||
|
|
||||||
|
CONFLICT_TYPES = {
|
||||||
|
"old" => "conflict_marker_their",
|
||||||
|
"new" => "conflict_marker_our"
|
||||||
|
}.freeze
|
||||||
|
|
||||||
attr_reader :merge_request
|
attr_reader :merge_request
|
||||||
|
|
||||||
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
|
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
|
||||||
|
@ -46,6 +51,34 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def diff_lines_for_serializer
|
||||||
|
# calculate sections and highlight lines before changing types
|
||||||
|
sections && highlight_lines!
|
||||||
|
|
||||||
|
sections.flat_map do |section|
|
||||||
|
if section[:conflict]
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
initial_type = nil
|
||||||
|
section[:lines].each do |line|
|
||||||
|
if line.type != initial_type
|
||||||
|
lines << create_separator_line(line)
|
||||||
|
initial_type = line.type
|
||||||
|
end
|
||||||
|
|
||||||
|
line.type = CONFLICT_TYPES[line.type]
|
||||||
|
lines << line
|
||||||
|
end
|
||||||
|
|
||||||
|
lines << create_separator_line(lines.last)
|
||||||
|
|
||||||
|
lines
|
||||||
|
else
|
||||||
|
section[:lines]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def sections
|
def sections
|
||||||
return @sections if @sections
|
return @sections if @sections
|
||||||
|
|
||||||
|
@ -93,9 +126,15 @@ module Gitlab
|
||||||
|
|
||||||
lines = tail_lines
|
lines = tail_lines
|
||||||
elsif conflict_before
|
elsif conflict_before
|
||||||
# We're at the end of the file (no conflicts after), so just remove extra
|
# We're at the end of the file (no conflicts after)
|
||||||
# trailing lines.
|
number_of_trailing_lines = lines.size
|
||||||
|
|
||||||
|
# Remove extra trailing lines
|
||||||
lines = lines.first(CONTEXT_LINES)
|
lines = lines.first(CONTEXT_LINES)
|
||||||
|
|
||||||
|
if number_of_trailing_lines > CONTEXT_LINES
|
||||||
|
lines << create_match_line(lines.last)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -117,6 +156,10 @@ module Gitlab
|
||||||
Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
|
Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_separator_line(line)
|
||||||
|
Gitlab::Diff::Line.new('', 'conflict_marker', line.index, nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
# Any line beginning with a letter, an underscore, or a dollar can be used in a
|
# Any line beginning with a letter, an underscore, or a dollar can be used in a
|
||||||
# match line header. Only context sections can contain match lines, as match lines
|
# match line header. Only context sections can contain match lines, as match lines
|
||||||
# have to exist in both versions of the file.
|
# have to exist in both versions of the file.
|
||||||
|
|
|
@ -8,9 +8,9 @@ module Gitlab
|
||||||
#
|
#
|
||||||
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
|
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
|
||||||
|
|
||||||
attr_reader :line_code, :type, :old_pos, :new_pos
|
attr_reader :line_code, :old_pos, :new_pos
|
||||||
attr_writer :rich_text
|
attr_writer :rich_text
|
||||||
attr_accessor :text, :index
|
attr_accessor :text, :index, :type
|
||||||
|
|
||||||
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
|
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
|
||||||
@text, @type, @index = text, type, index
|
@text, @type, @index = text, type, index
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module EtagCaching
|
module EtagCaching
|
||||||
class Middleware
|
class Middleware
|
||||||
|
SKIP_HEADER_KEY = 'X-Gitlab-Skip-Etag'
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def skip!(response)
|
||||||
|
response.set_header(SKIP_HEADER_KEY, '1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(app)
|
def initialize(app)
|
||||||
@app = app
|
@app = app
|
||||||
end
|
end
|
||||||
|
@ -22,9 +30,7 @@ module Gitlab
|
||||||
else
|
else
|
||||||
track_cache_miss(if_none_match, cached_value_present, route)
|
track_cache_miss(if_none_match, cached_value_present, route)
|
||||||
|
|
||||||
status, headers, body = @app.call(env)
|
maybe_apply_etag(etag, *@app.call(env))
|
||||||
headers['ETag'] = etag
|
|
||||||
[status, headers, body]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -43,6 +49,13 @@ module Gitlab
|
||||||
[weak_etag_format(current_value), cached_value_present]
|
[weak_etag_format(current_value), cached_value_present]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_apply_etag(etag, status, headers, body)
|
||||||
|
headers['ETag'] = etag unless
|
||||||
|
Gitlab::Utils.to_boolean(headers.delete(SKIP_HEADER_KEY))
|
||||||
|
|
||||||
|
[status, headers, body]
|
||||||
|
end
|
||||||
|
|
||||||
def weak_etag_format(value)
|
def weak_etag_format(value)
|
||||||
%Q{W/"#{value}"}
|
%Q{W/"#{value}"}
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,8 @@ module Gitlab
|
||||||
module Graphql
|
module Graphql
|
||||||
module Present
|
module Present
|
||||||
class Instrumentation
|
class Instrumentation
|
||||||
|
SAFE_CONTEXT_KEYS = %i[current_user].freeze
|
||||||
|
|
||||||
def instrument(type, field)
|
def instrument(type, field)
|
||||||
return field unless field.metadata[:type_class]
|
return field unless field.metadata[:type_class]
|
||||||
|
|
||||||
|
@ -22,7 +24,8 @@ module Gitlab
|
||||||
next old_resolver.call(presented_type, args, context)
|
next old_resolver.call(presented_type, args, context)
|
||||||
end
|
end
|
||||||
|
|
||||||
presenter = presented_in.presenter_class.new(object, **context.to_h)
|
attrs = safe_context_values(context)
|
||||||
|
presenter = presented_in.presenter_class.new(object, **attrs)
|
||||||
|
|
||||||
# we have to use the new `authorized_new` method, as `new` is protected
|
# we have to use the new `authorized_new` method, as `new` is protected
|
||||||
wrapped = presented_type.class.authorized_new(presenter, context)
|
wrapped = presented_type.class.authorized_new(presenter, context)
|
||||||
|
@ -34,6 +37,12 @@ module Gitlab
|
||||||
resolve(resolve_with_presenter)
|
resolve(resolve_with_presenter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def safe_context_values(context)
|
||||||
|
context.to_h.slice(*SAFE_CONTEXT_KEYS)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,20 +32,20 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.http_requests_total
|
def self.http_requests_total
|
||||||
@http_requests_total ||= ::Gitlab::Metrics.counter(:http_requests_total, 'Request count')
|
::Gitlab::Metrics.counter(:http_requests_total, 'Request count')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.rack_uncaught_errors_count
|
def self.rack_uncaught_errors_count
|
||||||
@rack_uncaught_errors_count ||= ::Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count')
|
::Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.http_request_duration_seconds
|
def self.http_request_duration_seconds
|
||||||
@http_request_duration_seconds ||= ::Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time',
|
::Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time',
|
||||||
{}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 2.5, 5, 10, 25])
|
{}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 2.5, 5, 10, 25])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.http_health_requests_total
|
def self.http_health_requests_total
|
||||||
@http_health_requests_total ||= ::Gitlab::Metrics.counter(:http_health_requests_total, 'Health endpoint request count')
|
::Gitlab::Metrics.counter(:http_health_requests_total, 'Health endpoint request count')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.initialize_metrics
|
def self.initialize_metrics
|
||||||
|
|
|
@ -16,7 +16,7 @@ module Gitlab
|
||||||
# Add process id params
|
# Add process id params
|
||||||
job['pid'] = ::Process.pid
|
job['pid'] = ::Process.pid
|
||||||
|
|
||||||
job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS']
|
job.delete('args') unless SidekiqLogArguments.enabled?
|
||||||
|
|
||||||
job
|
job
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,42 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Gitlab::UsageDataCounters
|
module Gitlab::UsageDataCounters
|
||||||
class DesignsCounter
|
class DesignsCounter < BaseCounter
|
||||||
extend Gitlab::UsageDataCounters::RedisCounter
|
|
||||||
|
|
||||||
KNOWN_EVENTS = %w[create update delete].freeze
|
KNOWN_EVENTS = %w[create update delete].freeze
|
||||||
|
PREFIX = 'design_management_designs'
|
||||||
UnknownEvent = Class.new(StandardError)
|
|
||||||
|
|
||||||
class << self
|
|
||||||
# Each event gets a unique Redis key
|
|
||||||
def redis_key(event)
|
|
||||||
raise UnknownEvent, event unless KNOWN_EVENTS.include?(event.to_s)
|
|
||||||
|
|
||||||
"USAGE_DESIGN_MANAGEMENT_DESIGNS_#{event}".upcase
|
|
||||||
end
|
|
||||||
|
|
||||||
def count(event)
|
|
||||||
increment(redis_key(event))
|
|
||||||
end
|
|
||||||
|
|
||||||
def read(event)
|
|
||||||
total_count(redis_key(event))
|
|
||||||
end
|
|
||||||
|
|
||||||
def totals
|
|
||||||
KNOWN_EVENTS.map { |event| [counter_key(event), read(event)] }.to_h
|
|
||||||
end
|
|
||||||
|
|
||||||
def fallback_totals
|
|
||||||
KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def counter_key(event)
|
|
||||||
"design_management_designs_#{event}".to_sym
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,54 +2,43 @@
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module UsageDataCounters
|
module UsageDataCounters
|
||||||
class WebIdeCounter
|
class WebIdeCounter < BaseCounter
|
||||||
extend RedisCounter
|
KNOWN_EVENTS = %w[commits views merge_requests previews terminals pipelines].freeze
|
||||||
KNOWN_EVENTS = %i[commits views merge_requests previews terminals pipelines].freeze
|
|
||||||
PREFIX = 'web_ide'
|
PREFIX = 'web_ide'
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def increment_commits_count
|
def increment_commits_count
|
||||||
increment(redis_key('commits'))
|
count('commits')
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_merge_requests_count
|
def increment_merge_requests_count
|
||||||
increment(redis_key('merge_requests'))
|
count('merge_requests')
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_views_count
|
def increment_views_count
|
||||||
increment(redis_key('views'))
|
count('views')
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_terminals_count
|
def increment_terminals_count
|
||||||
increment(redis_key('terminals'))
|
count('terminals')
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_pipelines_count
|
def increment_pipelines_count
|
||||||
increment(redis_key('pipelines'))
|
count('pipelines')
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_previews_count
|
def increment_previews_count
|
||||||
return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
|
return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
|
||||||
|
|
||||||
increment(redis_key('previews'))
|
count('previews')
|
||||||
end
|
|
||||||
|
|
||||||
def totals
|
|
||||||
KNOWN_EVENTS.map { |event| [counter_key(event), total_count(redis_key(event))] }.to_h
|
|
||||||
end
|
|
||||||
|
|
||||||
def fallback_totals
|
|
||||||
KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redis_key(event)
|
def redis_key(event)
|
||||||
"#{PREFIX}_#{event}_count".upcase
|
require_known_event(event)
|
||||||
end
|
|
||||||
|
|
||||||
def counter_key(event)
|
"#{prefix}_#{event}_count".upcase
|
||||||
"#{PREFIX}_#{event}".to_sym
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4696,9 +4696,6 @@ msgstr ""
|
||||||
msgid "By %{user_name}"
|
msgid "By %{user_name}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "By URL"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}"
|
msgid "By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5053,9 +5050,6 @@ msgstr ""
|
||||||
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
|
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Changing group URL can have unintended side effects."
|
msgid "Changing group URL can have unintended side effects."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6987,9 +6981,6 @@ msgstr ""
|
||||||
msgid "Compliance framework (optional)"
|
msgid "Compliance framework (optional)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Compliance frameworks"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "ComplianceDashboard|created by:"
|
msgid "ComplianceDashboard|created by:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10113,7 +10104,7 @@ msgstr ""
|
||||||
msgid "Enable"
|
msgid "Enable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab."
|
msgid "Enable %{linkStart}Gitpod%{linkEnd} integration to launch a development environment in your browser directly from GitLab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enable Auto DevOps"
|
msgid "Enable Auto DevOps"
|
||||||
|
@ -16149,6 +16140,9 @@ msgstr ""
|
||||||
msgid "Link title is required"
|
msgid "Link title is required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Link to an image"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Link to go to GitLab pipeline documentation"
|
msgid "Link to go to GitLab pipeline documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20313,18 +20307,12 @@ msgstr ""
|
||||||
msgid "Preferences|Choose what content you want to see on your homepage."
|
msgid "Preferences|Choose what content you want to see on your homepage."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Customize integrations with third party services."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
|
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Display time in 24-hour format"
|
msgid "Preferences|Display time in 24-hour format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Enable integrated code intelligence on code views"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Preferences|For example: 30 mins ago."
|
msgid "Preferences|For example: 30 mins ago."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20334,9 +20322,6 @@ msgstr ""
|
||||||
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
|
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Integrations"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Preferences|Layout width"
|
msgid "Preferences|Layout width"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20358,9 +20343,6 @@ msgstr ""
|
||||||
msgid "Preferences|Show whitespace changes in diffs"
|
msgid "Preferences|Show whitespace changes in diffs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Sourcegraph"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Preferences|Syntax highlighting theme"
|
msgid "Preferences|Syntax highlighting theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20415,6 +20397,9 @@ msgstr ""
|
||||||
msgid "Prevent users from changing their profile name"
|
msgid "Prevent users from changing their profile name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prevent users from modifying merge request approvers list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
|
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20544,6 +20529,24 @@ msgstr ""
|
||||||
msgid "Profile Settings"
|
msgid "Profile Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Customize integrations with third party services."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Enable Gitpod integration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Enable integrated code intelligence on code views"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Gitpod"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Integrations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProfilePreferences|Sourcegraph"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProfileSession|on"
|
msgid "ProfileSession|on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22331,9 +22334,6 @@ msgstr ""
|
||||||
msgid "Registry setup"
|
msgid "Registry setup"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reindexing status"
|
msgid "Reindexing status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25593,10 +25593,10 @@ msgstr ""
|
||||||
msgid "SourcegraphPreferences|This feature is experimental."
|
msgid "SourcegraphPreferences|This feature is experimental."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}."
|
msgid "SourcegraphPreferences|Uses %{linkStart}Sourcegraph.com%{linkEnd}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}."
|
msgid "SourcegraphPreferences|Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Spam Logs"
|
msgid "Spam Logs"
|
||||||
|
@ -26765,9 +26765,6 @@ msgstr ""
|
||||||
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
|
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "The above settings apply to all projects with the selected compliance framework(s)."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
|
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27085,6 +27082,9 @@ msgstr ""
|
||||||
msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
|
msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The tag name can't be changed for an existing release."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
|
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -29180,6 +29180,9 @@ msgstr ""
|
||||||
msgid "Upload a private key for your certificate"
|
msgid "Upload a private key for your certificate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Upload an image"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Upload file"
|
msgid "Upload file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -383,6 +383,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
environment: nil,
|
environment: nil,
|
||||||
merge_request: merge_request,
|
merge_request: merge_request,
|
||||||
diff_view: :inline,
|
diff_view: :inline,
|
||||||
|
merge_ref_head_diff: nil,
|
||||||
pagination_data: {
|
pagination_data: {
|
||||||
current_page: nil,
|
current_page: nil,
|
||||||
next_page: nil,
|
next_page: nil,
|
||||||
|
|
|
@ -113,6 +113,8 @@ RSpec.describe Projects::NotesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the first page of notes' do
|
it 'returns the first page of notes' do
|
||||||
|
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
|
||||||
|
|
||||||
get :index, params: request_params
|
get :index, params: request_params
|
||||||
|
|
||||||
expect(json_response['notes'].count).to eq(page_1.count)
|
expect(json_response['notes'].count).to eq(page_1.count)
|
||||||
|
@ -122,6 +124,8 @@ RSpec.describe Projects::NotesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the second page of notes' do
|
it 'returns the second page of notes' do
|
||||||
|
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
|
||||||
|
|
||||||
request.headers['X-Last-Fetched-At'] = page_1_boundary
|
request.headers['X-Last-Fetched-At'] = page_1_boundary
|
||||||
|
|
||||||
get :index, params: request_params
|
get :index, params: request_params
|
||||||
|
@ -133,6 +137,8 @@ RSpec.describe Projects::NotesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the final page of notes' do
|
it 'returns the final page of notes' do
|
||||||
|
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
|
||||||
|
|
||||||
request.headers['X-Last-Fetched-At'] = page_2_boundary
|
request.headers['X-Last-Fetched-At'] = page_2_boundary
|
||||||
|
|
||||||
get :index, params: request_params
|
get :index, params: request_params
|
||||||
|
@ -142,6 +148,19 @@ RSpec.describe Projects::NotesController do
|
||||||
expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now))
|
expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now))
|
||||||
expect(response.headers['Poll-Interval'].to_i).to be > 1
|
expect(response.headers['Poll-Interval'].to_i).to be > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an empty page of notes' do
|
||||||
|
expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!)
|
||||||
|
|
||||||
|
request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now)
|
||||||
|
|
||||||
|
get :index, params: request_params
|
||||||
|
|
||||||
|
expect(json_response['notes']).to be_empty
|
||||||
|
expect(json_response['more']).to be_falsy
|
||||||
|
expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now))
|
||||||
|
expect(response.headers['Poll-Interval'].to_i).to be > 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'feature flag disabled' do
|
context 'feature flag disabled' do
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`IntegrationView component should render IntegrationView properly 1`] = `
|
||||||
|
<div
|
||||||
|
name="sourcegraph"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label-bold"
|
||||||
|
>
|
||||||
|
|
||||||
|
Foo
|
||||||
|
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<gl-link-stub
|
||||||
|
class="has-tooltip"
|
||||||
|
href="http://foo.com/help"
|
||||||
|
title="More information"
|
||||||
|
>
|
||||||
|
<gl-icon-stub
|
||||||
|
class="vertical-align-middle"
|
||||||
|
name="question-o"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
</gl-link-stub>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-group form-check"
|
||||||
|
data-testid="profile-preferences-integration-form-group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
data-testid="profile-preferences-integration-hidden-field"
|
||||||
|
name="user[foo_enabled]"
|
||||||
|
type="hidden"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
data-testid="profile-preferences-integration-checkbox"
|
||||||
|
id="user_foo_enabled"
|
||||||
|
name="user[foo_enabled]"
|
||||||
|
type="checkbox"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="user_foo_enabled"
|
||||||
|
>
|
||||||
|
|
||||||
|
Enable foo
|
||||||
|
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<gl-form-text-stub
|
||||||
|
tag="div"
|
||||||
|
textvariant="muted"
|
||||||
|
>
|
||||||
|
<integration-help-text-stub
|
||||||
|
message="Click %{linkStart}Foo%{linkEnd}!"
|
||||||
|
messageurl="http://foo.com"
|
||||||
|
/>
|
||||||
|
</gl-form-text-stub>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ProfilePreferences component should render ProfilePreferences properly 1`] = `
|
||||||
|
<div
|
||||||
|
class="row gl-mt-3 js-preferences-form"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-sm-12"
|
||||||
|
>
|
||||||
|
<hr
|
||||||
|
data-testid="profile-preferences-integrations-rule"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-lg-4 profile-settings-sidebar"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="gl-mt-0"
|
||||||
|
data-testid="profile-preferences-integrations-heading"
|
||||||
|
>
|
||||||
|
|
||||||
|
Integrations
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Customize integrations with third party services.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-lg-8"
|
||||||
|
>
|
||||||
|
<integration-view-stub
|
||||||
|
config="[object Object]"
|
||||||
|
helplink="http://foo.com/help"
|
||||||
|
message="Click %{linkStart}Foo%{linkEnd}!"
|
||||||
|
messageurl="http://foo.com"
|
||||||
|
/>
|
||||||
|
<integration-view-stub
|
||||||
|
config="[object Object]"
|
||||||
|
helplink="http://bar.com/help"
|
||||||
|
message="Click %{linkStart}Bar%{linkEnd}!"
|
||||||
|
messageurl="http://bar.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import { GlFormText } from '@gitlab/ui';
|
||||||
|
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
|
||||||
|
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||||
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
|
import { integrationViews, userFields } from '../mock_data';
|
||||||
|
|
||||||
|
const viewProps = convertObjectPropsToCamelCase(integrationViews[0]);
|
||||||
|
|
||||||
|
describe('IntegrationView component', () => {
|
||||||
|
let wrapper;
|
||||||
|
const defaultProps = {
|
||||||
|
config: {
|
||||||
|
title: 'Foo',
|
||||||
|
label: 'Enable foo',
|
||||||
|
formName: 'foo_enabled',
|
||||||
|
},
|
||||||
|
...viewProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createComponent(options = {}) {
|
||||||
|
const { props = {}, provide = {} } = options;
|
||||||
|
return shallowMount(IntegrationView, {
|
||||||
|
provide: {
|
||||||
|
userFields,
|
||||||
|
...provide,
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCheckbox() {
|
||||||
|
return wrapper.find('[data-testid="profile-preferences-integration-checkbox"]');
|
||||||
|
}
|
||||||
|
function findFormGroup() {
|
||||||
|
return wrapper.find('[data-testid="profile-preferences-integration-form-group"]');
|
||||||
|
}
|
||||||
|
function findHiddenField() {
|
||||||
|
return wrapper.find('[data-testid="profile-preferences-integration-hidden-field"]');
|
||||||
|
}
|
||||||
|
function findFormGroupLabel() {
|
||||||
|
return wrapper.find('[data-testid="profile-preferences-integration-form-group"] label');
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the title correctly', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(wrapper.find('label.label-bold').text()).toBe('Foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the form correctly', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(findFormGroup().exists()).toBe(true);
|
||||||
|
expect(findHiddenField().exists()).toBe(true);
|
||||||
|
expect(findCheckbox().exists()).toBe(true);
|
||||||
|
expect(findCheckbox().attributes('id')).toBe('user_foo_enabled');
|
||||||
|
expect(findCheckbox().attributes('name')).toBe('user[foo_enabled]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the checkbox value to be set to 1', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(findCheckbox().attributes('value')).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the hidden value to be set to 0', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(findHiddenField().attributes('value')).toBe('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the checkbox value to be true', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(findCheckbox().element.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the checkbox value to be false when false is provided', () => {
|
||||||
|
wrapper = createComponent({
|
||||||
|
provide: {
|
||||||
|
userFields: {
|
||||||
|
foo_enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findCheckbox().element.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the checkbox value to be false when not provided', () => {
|
||||||
|
wrapper = createComponent({ provide: { userFields: {} } });
|
||||||
|
|
||||||
|
expect(findCheckbox().element.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the help text', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(wrapper.find(GlFormText).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(IntegrationHelpText).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the label correctly', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(findFormGroupLabel().text()).toBe('Enable foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render IntegrationView properly', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
|
||||||
|
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
|
||||||
|
import { integrationViews, userFields } from '../mock_data';
|
||||||
|
|
||||||
|
describe('ProfilePreferences component', () => {
|
||||||
|
let wrapper;
|
||||||
|
const defaultProvide = {
|
||||||
|
integrationViews: [],
|
||||||
|
userFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createComponent(options = {}) {
|
||||||
|
const { props = {}, provide = {} } = options;
|
||||||
|
return shallowMount(ProfilePreferences, {
|
||||||
|
provide: {
|
||||||
|
...defaultProvide,
|
||||||
|
...provide,
|
||||||
|
},
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render Integrations section', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
const views = wrapper.findAll(IntegrationView);
|
||||||
|
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
|
||||||
|
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
|
||||||
|
|
||||||
|
expect(divider.exists()).toBe(false);
|
||||||
|
expect(heading.exists()).toBe(false);
|
||||||
|
expect(views).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Integration section', () => {
|
||||||
|
wrapper = createComponent({ provide: { integrationViews } });
|
||||||
|
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
|
||||||
|
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
|
||||||
|
const views = wrapper.findAll(IntegrationView);
|
||||||
|
|
||||||
|
expect(divider.exists()).toBe(true);
|
||||||
|
expect(heading.exists()).toBe(true);
|
||||||
|
expect(views).toHaveLength(integrationViews.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render ProfilePreferences properly', () => {
|
||||||
|
wrapper = createComponent({ provide: { integrationViews } });
|
||||||
|
|
||||||
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const integrationViews = [
|
||||||
|
{
|
||||||
|
name: 'sourcegraph',
|
||||||
|
help_link: 'http://foo.com/help',
|
||||||
|
message: 'Click %{linkStart}Foo%{linkEnd}!',
|
||||||
|
message_url: 'http://foo.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gitpod',
|
||||||
|
help_link: 'http://bar.com/help',
|
||||||
|
message: 'Click %{linkStart}Bar%{linkEnd}!',
|
||||||
|
message_url: 'http://bar.com',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const userFields = {
|
||||||
|
foo_enabled: true,
|
||||||
|
};
|
|
@ -24,7 +24,6 @@ describe('Release edit/new component', () => {
|
||||||
state = {
|
state = {
|
||||||
release,
|
release,
|
||||||
markdownDocsPath: 'path/to/markdown/docs',
|
markdownDocsPath: 'path/to/markdown/docs',
|
||||||
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
|
|
||||||
releasesPagePath: 'path/to/releases/page',
|
releasesPagePath: 'path/to/releases/page',
|
||||||
projectId: '8',
|
projectId: '8',
|
||||||
groupId: '42',
|
groupId: '42',
|
||||||
|
|
|
@ -6,7 +6,6 @@ import createStore from '~/releases/stores';
|
||||||
import createDetailModule from '~/releases/stores/modules/detail';
|
import createDetailModule from '~/releases/stores/modules/detail';
|
||||||
|
|
||||||
const TEST_TAG_NAME = 'test-tag-name';
|
const TEST_TAG_NAME = 'test-tag-name';
|
||||||
const TEST_DOCS_PATH = '/help/test/docs/path';
|
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
|
@ -24,21 +23,11 @@ describe('releases/components/tag_field_existing', () => {
|
||||||
|
|
||||||
const findInput = () => wrapper.find(GlFormInput);
|
const findInput = () => wrapper.find(GlFormInput);
|
||||||
const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
|
const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
|
||||||
const findHelpLink = () => {
|
|
||||||
const link = findHelp().find('a');
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: link.text(),
|
|
||||||
href: link.attributes('href'),
|
|
||||||
target: link.attributes('target'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = createStore({
|
store = createStore({
|
||||||
modules: {
|
modules: {
|
||||||
detail: createDetailModule({
|
detail: createDetailModule({
|
||||||
updateReleaseApiDocsPath: TEST_DOCS_PATH,
|
|
||||||
tagName: TEST_TAG_NAME,
|
tagName: TEST_TAG_NAME,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -68,16 +57,8 @@ describe('releases/components/tag_field_existing', () => {
|
||||||
createComponent(mount);
|
createComponent(mount);
|
||||||
|
|
||||||
expect(findHelp().text()).toMatchInterpolatedText(
|
expect(findHelp().text()).toMatchInterpolatedText(
|
||||||
'Changing a Release tag is only supported via Releases API. More information',
|
"The tag name can't be changed for an existing release.",
|
||||||
);
|
);
|
||||||
|
|
||||||
const helpLink = findHelpLink();
|
|
||||||
|
|
||||||
expect(helpLink).toEqual({
|
|
||||||
text: 'More information',
|
|
||||||
href: TEST_DOCS_PATH,
|
|
||||||
target: '_blank',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,7 +47,6 @@ describe('Release detail actions', () => {
|
||||||
releasesPagePath: 'path/to/releases/page',
|
releasesPagePath: 'path/to/releases/page',
|
||||||
markdownDocsPath: 'path/to/markdown/docs',
|
markdownDocsPath: 'path/to/markdown/docs',
|
||||||
markdownPreviewPath: 'path/to/markdown/preview',
|
markdownPreviewPath: 'path/to/markdown/preview',
|
||||||
updateReleaseApiDocsPath: 'path/to/api/docs',
|
|
||||||
}),
|
}),
|
||||||
...getters,
|
...getters,
|
||||||
...rootState,
|
...rootState,
|
||||||
|
|
|
@ -18,7 +18,6 @@ describe('Release detail mutations', () => {
|
||||||
releasesPagePath: 'path/to/releases/page',
|
releasesPagePath: 'path/to/releases/page',
|
||||||
markdownDocsPath: 'path/to/markdown/docs',
|
markdownDocsPath: 'path/to/markdown/docs',
|
||||||
markdownPreviewPath: 'path/to/markdown/preview',
|
markdownPreviewPath: 'path/to/markdown/preview',
|
||||||
updateReleaseApiDocsPath: 'path/to/api/docs',
|
|
||||||
});
|
});
|
||||||
release = convertObjectPropsToCamelCase(originalRelease);
|
release = convertObjectPropsToCamelCase(originalRelease);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { mounts, project, branch, baseUrl } from '../../mock_data';
|
||||||
|
|
||||||
describe('rich_content_editor/renderers/render_image', () => {
|
describe('rich_content_editor/renderers/render_image', () => {
|
||||||
let renderer;
|
let renderer;
|
||||||
|
let imageRepository;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
renderer = imageRenderer.build(mounts, project, branch, baseUrl);
|
renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository);
|
||||||
|
imageRepository = { get: () => null };
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('build', () => {
|
describe('build', () => {
|
||||||
|
@ -27,6 +29,21 @@ describe('rich_content_editor/renderers/render_image', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
|
let skipChildren;
|
||||||
|
let context;
|
||||||
|
let node;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
skipChildren = jest.fn();
|
||||||
|
context = { skipChildren };
|
||||||
|
node = {
|
||||||
|
firstChild: {
|
||||||
|
type: 'img',
|
||||||
|
literal: 'Some Image',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
destination | isAbsolute | src
|
destination | isAbsolute | src
|
||||||
${'http://test.host/absolute/path/to/image.png'} | ${true} | ${'http://test.host/absolute/path/to/image.png'}
|
${'http://test.host/absolute/path/to/image.png'} | ${true} | ${'http://test.host/absolute/path/to/image.png'}
|
||||||
|
@ -36,15 +53,8 @@ describe('rich_content_editor/renderers/render_image', () => {
|
||||||
${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/./relative/to/current/image.png'}
|
${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/./relative/to/current/image.png'}
|
||||||
${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/../relative/to/current/image.png'}
|
${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/../relative/to/current/image.png'}
|
||||||
`('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => {
|
`('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => {
|
||||||
const skipChildren = jest.fn();
|
node.destination = destination;
|
||||||
const context = { skipChildren };
|
|
||||||
const node = {
|
|
||||||
destination,
|
|
||||||
firstChild: {
|
|
||||||
type: 'img',
|
|
||||||
literal: 'Some Image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const result = renderer.render(node, context);
|
const result = renderer.render(node, context);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
@ -60,5 +70,27 @@ describe('rich_content_editor/renderers/render_image', () => {
|
||||||
|
|
||||||
expect(skipChildren).toHaveBeenCalled();
|
expect(skipChildren).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders an image if a cached image is found in the repository, use the base64 content as the source', () => {
|
||||||
|
const imageContent = 'some-content';
|
||||||
|
const originalSrc = 'path/to/image.png';
|
||||||
|
|
||||||
|
imageRepository.get = () => imageContent;
|
||||||
|
renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository);
|
||||||
|
node.destination = originalSrc;
|
||||||
|
|
||||||
|
const result = renderer.render(node, context);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'openTag',
|
||||||
|
tagName: 'img',
|
||||||
|
selfClose: true,
|
||||||
|
attributes: {
|
||||||
|
'data-original-src': originalSrc,
|
||||||
|
src: `data:image;base64,${imageContent}`,
|
||||||
|
alt: 'Some Image',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`IntegrationHelpText component should not render the link when start and end is not provided 1`] = `
|
||||||
|
<span>
|
||||||
|
Click nowhere!
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`IntegrationHelpText component should render the help text 1`] = `
|
||||||
|
<span>
|
||||||
|
Click
|
||||||
|
<gl-link-stub
|
||||||
|
href="http://bar.com"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
|
||||||
|
Bar
|
||||||
|
|
||||||
|
<gl-icon-stub
|
||||||
|
class="gl-vertical-align-middle"
|
||||||
|
name="external-link"
|
||||||
|
size="12"
|
||||||
|
/>
|
||||||
|
</gl-link-stub>
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
`;
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
|
||||||
|
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||||
|
|
||||||
|
describe('IntegrationHelpText component', () => {
|
||||||
|
let wrapper;
|
||||||
|
const defaultProps = {
|
||||||
|
message: 'Click %{linkStart}Bar%{linkEnd}!',
|
||||||
|
messageUrl: 'http://bar.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createComponent(props = {}) {
|
||||||
|
return shallowMount(IntegrationHelpText, {
|
||||||
|
propsData: {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
GlSprintf,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the gl components', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(wrapper.find(GlSprintf).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(GlIcon).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(GlLink).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the help text', () => {
|
||||||
|
wrapper = createComponent();
|
||||||
|
|
||||||
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use the gl-link and gl-icon components', () => {
|
||||||
|
wrapper = createComponent({ message: 'Click nowhere!' });
|
||||||
|
|
||||||
|
expect(wrapper.find(GlSprintf).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(GlIcon).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(GlLink).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render the link when start and end is not provided', () => {
|
||||||
|
wrapper = createComponent({ message: 'Click nowhere!' });
|
||||||
|
|
||||||
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -91,12 +91,25 @@ describe('Editor Service', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addImage', () => {
|
describe('addImage', () => {
|
||||||
it('calls the exec method on the instance', () => {
|
const file = new File([], 'some-file.jpg');
|
||||||
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
|
const mockImage = { imageUrl: 'some/url.png', altText: 'some alt text' };
|
||||||
|
|
||||||
addImage(mockInstance, mockImage);
|
it('calls the insertElement method on the squire instance when in WYSIWYG mode', () => {
|
||||||
|
jest.spyOn(URL, 'createObjectURL');
|
||||||
|
mockInstance.editor.isWysiwygMode.mockReturnValue(true);
|
||||||
|
mockInstance.editor.getSquire.mockReturnValue({ insertElement: jest.fn() });
|
||||||
|
|
||||||
expect(mockInstance.editor.exec).toHaveBeenCalledWith('AddImage', mockImage);
|
addImage(mockInstance, mockImage, file);
|
||||||
|
|
||||||
|
expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalled();
|
||||||
|
expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the insertText method on the instance when in Markdown mode', () => {
|
||||||
|
mockInstance.editor.isWysiwygMode.mockReturnValue(false);
|
||||||
|
addImage(mockInstance, mockImage, file);
|
||||||
|
|
||||||
|
expect(mockInstance.editor.insertText).toHaveBeenCalledWith('![some alt text](some/url.png)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,7 @@ describe('Add Image Modal', () => {
|
||||||
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
|
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallowMount(AddImageModal, {
|
wrapper = shallowMount(AddImageModal, { propsData });
|
||||||
provide: { glFeatures: { sseImageUploads: true } },
|
|
||||||
propsData,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when content is loaded', () => {
|
describe('when content is loaded', () => {
|
||||||
|
|
|
@ -180,7 +180,7 @@ describe('Rich Content Editor', () => {
|
||||||
wrapper.vm.$refs.editor = mockInstance;
|
wrapper.vm.$refs.editor = mockInstance;
|
||||||
|
|
||||||
findAddImageModal().vm.$emit('addImage', mockImage);
|
findAddImageModal().vm.$emit('addImage', mockImage);
|
||||||
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
|
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ::CachingArrayResolver do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be(:non_admins) { create_list(:user, 4, admin: false) }
|
||||||
|
let(:query_context) { {} }
|
||||||
|
let(:max_page_size) { 10 }
|
||||||
|
let(:field) { double('Field', max_page_size: max_page_size) }
|
||||||
|
let(:schema) { double('Schema', default_max_page_size: 3) }
|
||||||
|
|
||||||
|
let_it_be(:caching_resolver) do
|
||||||
|
mod = described_class
|
||||||
|
|
||||||
|
Class.new(::Resolvers::BaseResolver) do
|
||||||
|
include mod
|
||||||
|
|
||||||
|
def query_input(is_admin:)
|
||||||
|
is_admin
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_for(is_admin)
|
||||||
|
if is_admin.nil?
|
||||||
|
model_class.all
|
||||||
|
else
|
||||||
|
model_class.where(admin: is_admin)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def model_class
|
||||||
|
User # Happens to include FromUnion, and is cheap-ish to create
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
context 'there are more than MAX_UNION_SIZE queries' do
|
||||||
|
let_it_be(:max_union) { 3 }
|
||||||
|
let_it_be(:resolver) do
|
||||||
|
mod = described_class
|
||||||
|
max = max_union
|
||||||
|
|
||||||
|
Class.new(::Resolvers::BaseResolver) do
|
||||||
|
include mod
|
||||||
|
|
||||||
|
def query_input(username:)
|
||||||
|
username
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_for(username)
|
||||||
|
if username.nil?
|
||||||
|
model_class.all
|
||||||
|
else
|
||||||
|
model_class.where(username: username)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def model_class
|
||||||
|
User # Happens to include FromUnion, and is cheap-ish to create
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method :max_union_size do
|
||||||
|
max
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'executes the queries in multiple batches' do
|
||||||
|
users = create_list(:user, (max_union * 2) + 1)
|
||||||
|
expect(User).to receive(:from_union).twice.and_call_original
|
||||||
|
|
||||||
|
results = users.in_groups_of(2, false).map do |users|
|
||||||
|
resolve(resolver, args: { username: users.map(&:username) }, field: field, schema: schema)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(results.flat_map(&method(:force))).to match_array(users)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'all queries return results' do
|
||||||
|
let_it_be(:admins) { create_list(:admin, 3) }
|
||||||
|
|
||||||
|
it 'batches the queries' do
|
||||||
|
expect do
|
||||||
|
[resolve_users(true), resolve_users(false)].each(&method(:force))
|
||||||
|
end.to issue_same_number_of_queries_as { force(resolve_users(nil)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds the correct values' do
|
||||||
|
found_admins = resolve_users(true)
|
||||||
|
found_others = resolve_users(false)
|
||||||
|
admins_again = resolve_users(true)
|
||||||
|
found_all = resolve_users(nil)
|
||||||
|
|
||||||
|
expect(force(found_admins)).to match_array(admins)
|
||||||
|
expect(force(found_others)).to match_array(non_admins)
|
||||||
|
expect(force(admins_again)).to match_array(admins)
|
||||||
|
expect(force(found_all)).to match_array(admins + non_admins)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not perform a union of a query with itself' do
|
||||||
|
expect(User).to receive(:where).once.and_call_original
|
||||||
|
|
||||||
|
[resolve_users(false), resolve_users(false)].each(&method(:force))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'one of the queries returns no results' do
|
||||||
|
it 'finds the correct values' do
|
||||||
|
found_admins = resolve_users(true)
|
||||||
|
found_others = resolve_users(false)
|
||||||
|
found_all = resolve_users(nil)
|
||||||
|
|
||||||
|
expect(force(found_admins)).to be_empty
|
||||||
|
expect(force(found_others)).to match_array(non_admins)
|
||||||
|
expect(force(found_all)).to match_array(non_admins)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'one of the queries has already been cached' do
|
||||||
|
before do
|
||||||
|
force(resolve_users(nil))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'avoids further queries' do
|
||||||
|
expect do
|
||||||
|
repeated_find = resolve_users(nil)
|
||||||
|
|
||||||
|
expect(force(repeated_find)).to match_array(non_admins)
|
||||||
|
end.not_to exceed_query_limit(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the resolver overrides item_found' do
|
||||||
|
let_it_be(:admins) { create_list(:admin, 2) }
|
||||||
|
let(:query_context) do
|
||||||
|
{
|
||||||
|
found: { true => [], false => [], nil => [] }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:with_item_found) do
|
||||||
|
Class.new(caching_resolver) do
|
||||||
|
def item_found(key, item)
|
||||||
|
context[:found][key] << item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'receives item_found for each key the item mapped to' do
|
||||||
|
found_admins = resolve_users(true, with_item_found)
|
||||||
|
found_all = resolve_users(nil, with_item_found)
|
||||||
|
|
||||||
|
[found_admins, found_all].each(&method(:force))
|
||||||
|
|
||||||
|
expect(query_context[:found]).to match({
|
||||||
|
false => be_empty,
|
||||||
|
true => match_array(admins),
|
||||||
|
nil => match_array(admins + non_admins)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the max_page_size is lower than the total result size' do
|
||||||
|
let(:max_page_size) { 2 }
|
||||||
|
|
||||||
|
it 'respects the max_page_size, on a per subset basis' do
|
||||||
|
found_all = resolve_users(nil)
|
||||||
|
found_others = resolve_users(false)
|
||||||
|
|
||||||
|
expect(force(found_all).size).to eq(2)
|
||||||
|
expect(force(found_others).size).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the field does not declare max_page_size' do
|
||||||
|
let(:max_page_size) { nil }
|
||||||
|
|
||||||
|
it 'takes the page size from schema.default_max_page_size' do
|
||||||
|
found_all = resolve_users(nil)
|
||||||
|
found_others = resolve_users(false)
|
||||||
|
|
||||||
|
expect(force(found_all).size).to eq(schema.default_max_page_size)
|
||||||
|
expect(force(found_others).size).to eq(schema.default_max_page_size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
specify 'force . resolve === to_a . query_for . query_input' do
|
||||||
|
r = resolver_instance(caching_resolver)
|
||||||
|
args = { is_admin: false }
|
||||||
|
|
||||||
|
naive = r.query_for(r.query_input(**args)).to_a
|
||||||
|
|
||||||
|
expect(force(r.resolve(**args))).to eq(naive)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_users(is_admin, resolver = caching_resolver)
|
||||||
|
args = { is_admin: is_admin }
|
||||||
|
resolve(resolver, args: args, field: field, ctx: query_context, schema: schema)
|
||||||
|
end
|
||||||
|
|
||||||
|
def force(lazy)
|
||||||
|
::Gitlab::Graphql::Lazy.force(lazy)
|
||||||
|
end
|
||||||
|
end
|
|
@ -71,7 +71,6 @@ RSpec.describe ReleasesHelper do
|
||||||
markdown_preview_path
|
markdown_preview_path
|
||||||
markdown_docs_path
|
markdown_docs_path
|
||||||
releases_page_path
|
releases_page_path
|
||||||
update_release_api_docs_path
|
|
||||||
release_assets_docs_path
|
release_assets_docs_path
|
||||||
manage_milestones_path
|
manage_milestones_path
|
||||||
new_milestone_path)
|
new_milestone_path)
|
||||||
|
@ -89,7 +88,6 @@ RSpec.describe ReleasesHelper do
|
||||||
releases_page_path
|
releases_page_path
|
||||||
markdown_preview_path
|
markdown_preview_path
|
||||||
markdown_docs_path
|
markdown_docs_path
|
||||||
update_release_api_docs_path
|
|
||||||
release_assets_docs_path
|
release_assets_docs_path
|
||||||
manage_milestones_path
|
manage_milestones_path
|
||||||
new_milestone_path
|
new_milestone_path
|
||||||
|
|
|
@ -5,60 +5,43 @@ require 'spec_helper'
|
||||||
RSpec.describe SourcegraphHelper do
|
RSpec.describe SourcegraphHelper do
|
||||||
describe '#sourcegraph_url_message' do
|
describe '#sourcegraph_url_message' do
|
||||||
let(:sourcegraph_url) { 'http://sourcegraph.example.com' }
|
let(:sourcegraph_url) { 'http://sourcegraph.example.com' }
|
||||||
|
let(:feature_conditional) { false }
|
||||||
|
let(:public_only) { false }
|
||||||
|
let(:is_com) { true }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url).and_return(sourcegraph_url)
|
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url).and_return(sourcegraph_url)
|
||||||
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url_is_com?).and_return(is_com)
|
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url_is_com?).and_return(is_com)
|
||||||
|
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_public_only).and_return(public_only)
|
||||||
|
allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { helper.sourcegraph_url_message }
|
subject { helper.sourcegraph_url_message }
|
||||||
|
|
||||||
context 'with .com sourcegraph url' do
|
context 'with .com sourcegraph url' do
|
||||||
let(:is_com) { true }
|
it { is_expected.to have_text('Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.') }
|
||||||
|
|
||||||
it { is_expected.to have_text('Uses Sourcegraph.com') }
|
|
||||||
it { is_expected.to have_link('Sourcegraph.com', href: sourcegraph_url) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with custom sourcegraph url' do
|
context 'with custom sourcegraph url' do
|
||||||
let(:is_com) { false }
|
let(:is_com) { false }
|
||||||
|
|
||||||
it { is_expected.to have_text('Uses a custom Sourcegraph instance') }
|
it { is_expected.to have_text('Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}. This feature is experimental.') }
|
||||||
it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) }
|
|
||||||
|
|
||||||
context 'with unsafe url' do
|
|
||||||
let(:sourcegraph_url) { '\" onload=\"alert(1);\"' }
|
|
||||||
|
|
||||||
it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) }
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#sourcegraph_experimental_message' do
|
|
||||||
let(:feature_conditional) { false }
|
|
||||||
let(:public_only) { false }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(Gitlab::CurrentSettings).to receive(:sourcegraph_public_only).and_return(public_only)
|
|
||||||
allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional)
|
|
||||||
end
|
|
||||||
|
|
||||||
subject { helper.sourcegraph_experimental_message }
|
|
||||||
|
|
||||||
context 'when not limited by feature or public only' do
|
context 'when not limited by feature or public only' do
|
||||||
it { is_expected.to eq "This feature is experimental." }
|
it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.' }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when limited by feature' do
|
context 'when limited by feature' do
|
||||||
let(:feature_conditional) { true }
|
let(:feature_conditional) { true }
|
||||||
|
|
||||||
it { is_expected.to eq "This feature is experimental and currently limited to certain projects." }
|
it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental and currently limited to certain projects.' }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when limited by public only' do
|
context 'when limited by public only' do
|
||||||
let(:public_only) { true }
|
let(:public_only) { true }
|
||||||
|
|
||||||
it { is_expected.to eq "This feature is experimental and limited to public projects." }
|
it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental and limited to public projects.' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,6 +93,51 @@ RSpec.describe Gitlab::Conflict::File do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#diff_lines_for_serializer' do
|
||||||
|
let(:diff_line_types) { conflict_file.diff_lines_for_serializer.map(&:type) }
|
||||||
|
|
||||||
|
it 'assigns conflict types to the diff lines' do
|
||||||
|
expect(diff_line_types[4]).to eq('conflict_marker')
|
||||||
|
expect(diff_line_types[5..10]).to eq(['conflict_marker_our'] * 6)
|
||||||
|
expect(diff_line_types[11]).to eq('conflict_marker')
|
||||||
|
expect(diff_line_types[12..17]).to eq(['conflict_marker_their'] * 6)
|
||||||
|
expect(diff_line_types[18]).to eq('conflict_marker')
|
||||||
|
|
||||||
|
expect(diff_line_types[19..24]).to eq([nil] * 6)
|
||||||
|
|
||||||
|
expect(diff_line_types[25]).to eq('conflict_marker')
|
||||||
|
expect(diff_line_types[26..27]).to eq(['conflict_marker_our'] * 2)
|
||||||
|
expect(diff_line_types[28]).to eq('conflict_marker')
|
||||||
|
expect(diff_line_types[29..30]).to eq(['conflict_marker_their'] * 2)
|
||||||
|
expect(diff_line_types[31]).to eq('conflict_marker')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add a match line to the end of the section' do
|
||||||
|
expect(diff_line_types.last).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are unchanged trailing lines' do
|
||||||
|
let(:rugged_conflict) { index.conflicts.first }
|
||||||
|
let(:raw_conflict_content) { index.merge_file('files/ruby/popen.rb')[:data] }
|
||||||
|
|
||||||
|
it 'assign conflict types and adds match line to the end of the section' do
|
||||||
|
expect(diff_line_types).to eq([
|
||||||
|
'match',
|
||||||
|
nil, nil, nil,
|
||||||
|
"conflict_marker",
|
||||||
|
"conflict_marker_our",
|
||||||
|
"conflict_marker",
|
||||||
|
"conflict_marker_their",
|
||||||
|
"conflict_marker_their",
|
||||||
|
"conflict_marker_their",
|
||||||
|
"conflict_marker",
|
||||||
|
nil, nil, nil,
|
||||||
|
"match"
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#sections' do
|
describe '#sections' do
|
||||||
it 'only inserts match lines when there is a gap between sections' do
|
it 'only inserts match lines when there is a gap between sections' do
|
||||||
conflict_file.sections.each_with_index do |section, i|
|
conflict_file.sections.each_with_index do |section, i|
|
||||||
|
|
|
@ -10,6 +10,17 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
|
||||||
let(:enabled_path) { '/gitlab-org/gitlab-foss/noteable/issue/1/notes' }
|
let(:enabled_path) { '/gitlab-org/gitlab-foss/noteable/issue/1/notes' }
|
||||||
let(:endpoint) { 'issue_notes' }
|
let(:endpoint) { 'issue_notes' }
|
||||||
|
|
||||||
|
describe '.skip!' do
|
||||||
|
it 'sets the skip header on the response' do
|
||||||
|
rsp = ActionDispatch::Response.new
|
||||||
|
rsp.set_header('Anything', 'Else')
|
||||||
|
|
||||||
|
described_class.skip!(rsp)
|
||||||
|
|
||||||
|
expect(rsp.headers.to_h).to eq(described_class::SKIP_HEADER_KEY => '1', 'Anything' => 'Else')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when ETag caching is not enabled for current route' do
|
context 'when ETag caching is not enabled for current route' do
|
||||||
let(:path) { '/gitlab-org/gitlab-foss/tree/master/noteable/issue/1/notes' }
|
let(:path) { '/gitlab-org/gitlab-foss/tree/master/noteable/issue/1/notes' }
|
||||||
|
|
||||||
|
@ -77,6 +88,28 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the matching route requests that the ETag is skipped' do
|
||||||
|
let(:path) { enabled_path }
|
||||||
|
let(:app) do
|
||||||
|
proc do |_env|
|
||||||
|
response = ActionDispatch::Response.new
|
||||||
|
|
||||||
|
described_class.skip!(response)
|
||||||
|
|
||||||
|
[200, response.headers.to_h, '']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the correct headers' do
|
||||||
|
expect(app).to receive(:call).and_call_original
|
||||||
|
|
||||||
|
_, headers, _ = middleware.call(build_request(path, if_none_match))
|
||||||
|
|
||||||
|
expect(headers).not_to have_key('ETag')
|
||||||
|
expect(headers).not_to have_key(described_class::SKIP_HEADER_KEY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
shared_examples 'sends a process_action.action_controller notification' do |status_code|
|
shared_examples 'sends a process_action.action_controller notification' do |status_code|
|
||||||
let(:expected_items) do
|
let(:expected_items) do
|
||||||
{
|
{
|
||||||
|
|
|
@ -127,7 +127,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.initialize_metrics', :prometheus, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/281164', type: :investigating } do
|
describe '.initialize_metrics', :prometheus do
|
||||||
it "sets labels for http_requests_total" do
|
it "sets labels for http_requests_total" do
|
||||||
expected_labels = []
|
expected_labels = []
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do
|
context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do
|
||||||
|
before do
|
||||||
|
stub_env('SIDEKIQ_LOG_ARGUMENTS', '0')
|
||||||
|
end
|
||||||
|
|
||||||
it 'logs start and end of job without args' do
|
it 'logs start and end of job without args' do
|
||||||
Timecop.freeze(timestamp) do
|
Timecop.freeze(timestamp) do
|
||||||
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
|
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
|
||||||
|
@ -150,8 +154,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
|
||||||
|
|
||||||
it 'logs with scheduling latency' do
|
it 'logs with scheduling latency' do
|
||||||
Timecop.freeze(timestamp) do
|
Timecop.freeze(timestamp) do
|
||||||
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
|
expect(logger).to receive(:info).with(start_payload).ordered
|
||||||
expect(logger).to receive(:info).with(end_payload.except('args')).ordered
|
expect(logger).to receive(:info).with(end_payload).ordered
|
||||||
expect(subject).to receive(:log_job_start).and_call_original
|
expect(subject).to receive(:log_job_start).and_call_original
|
||||||
expect(subject).to receive(:log_job_done).and_call_original
|
expect(subject).to receive(:log_job_done).and_call_original
|
||||||
|
|
||||||
|
@ -173,12 +177,12 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:expected_end_payload) do
|
let(:expected_end_payload) do
|
||||||
end_payload.except('args').merge(timing_data)
|
end_payload.merge(timing_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs with Gitaly and Rugged timing data' do
|
it 'logs with Gitaly and Rugged timing data' do
|
||||||
Timecop.freeze(timestamp) do
|
Timecop.freeze(timestamp) do
|
||||||
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
|
expect(logger).to receive(:info).with(start_payload).ordered
|
||||||
expect(logger).to receive(:info).with(expected_end_payload).ordered
|
expect(logger).to receive(:info).with(expected_end_payload).ordered
|
||||||
|
|
||||||
subject.call(job, 'test_queue') do
|
subject.call(job, 'test_queue') do
|
||||||
|
@ -194,10 +198,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
|
||||||
allow(Process).to receive(:clock_gettime).and_call_original
|
allow(Process).to receive(:clock_gettime).and_call_original
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:expected_start_payload) { start_payload.except('args') }
|
let(:expected_start_payload) { start_payload }
|
||||||
|
|
||||||
let(:expected_end_payload) do
|
let(:expected_end_payload) do
|
||||||
end_payload.except('args').merge('cpu_s' => a_value >= 0)
|
end_payload.merge('cpu_s' => a_value >= 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:expected_end_payload_with_db) do
|
let(:expected_end_payload_with_db) do
|
||||||
|
@ -228,10 +232,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there is extra metadata set for the done log' do
|
context 'when there is extra metadata set for the done log' do
|
||||||
let(:expected_start_payload) { start_payload.except('args') }
|
let(:expected_start_payload) { start_payload }
|
||||||
|
|
||||||
let(:expected_end_payload) do
|
let(:expected_end_payload) do
|
||||||
end_payload.except('args').merge("#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1" => 15, "#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2" => 16)
|
end_payload.merge("#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1" => 15, "#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2" => 16)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs it in the done log' do
|
it 'logs it in the done log' do
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Project noteable notes' do
|
||||||
|
describe '#index' do
|
||||||
|
let_it_be(:merge_request) { create(:merge_request) }
|
||||||
|
|
||||||
|
let(:etag_store) { Gitlab::EtagCaching::Store.new }
|
||||||
|
let(:notes_path) { project_noteable_notes_path(project, target_type: merge_request.class.name.underscore, target_id: merge_request.id) }
|
||||||
|
let(:project) { merge_request.project }
|
||||||
|
let(:user) { project.owner }
|
||||||
|
|
||||||
|
let(:response_etag) { response.headers['ETag'] }
|
||||||
|
let(:stored_etag) { "W/\"#{etag_store.get(notes_path)}\"" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not set a Gitlab::EtagCaching ETag if there is a note' do
|
||||||
|
create(:note_on_merge_request, noteable: merge_request, project: merge_request.project)
|
||||||
|
|
||||||
|
get notes_path
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
|
||||||
|
# Rack::ETag will set an etag based on the body digest, but that doesn't
|
||||||
|
# interfere with notes pagination
|
||||||
|
expect(response_etag).not_to eq(stored_etag)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets a Gitlab::EtagCaching ETag if there is no note' do
|
||||||
|
get notes_path
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response_etag).to eq(stored_etag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -69,4 +69,15 @@ RSpec.describe DiffFileEntity do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#is_fully_expanded' do
|
||||||
|
context 'file with a conflict' do
|
||||||
|
let(:options) { { conflicts: { diff_file.new_path => double(diff_lines_for_serializer: []) } } }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(diff_file).not_to receive(:fully_expanded?)
|
||||||
|
expect(subject[:is_fully_expanded]).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,9 +8,12 @@ RSpec.describe DiffsEntity do
|
||||||
let(:request) { EntityRequest.new(project: project, current_user: user) }
|
let(:request) { EntityRequest.new(project: project, current_user: user) }
|
||||||
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
||||||
let(:merge_request_diffs) { merge_request.merge_request_diffs }
|
let(:merge_request_diffs) { merge_request.merge_request_diffs }
|
||||||
|
let(:options) do
|
||||||
|
{ request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs }
|
||||||
|
end
|
||||||
|
|
||||||
let(:entity) do
|
let(:entity) do
|
||||||
described_class.new(merge_request_diffs.first.diffs, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
|
described_class.new(merge_request_diffs.first.diffs, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'as json' do
|
context 'as json' do
|
||||||
|
@ -68,5 +71,50 @@ RSpec.describe DiffsEntity do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when there are conflicts' do
|
||||||
|
let(:diff_files) { merge_request_diffs.first.diffs.diff_files }
|
||||||
|
let(:diff_file_with_conflict) { diff_files.to_a.last }
|
||||||
|
let(:diff_file_without_conflict) { diff_files.to_a[-2] }
|
||||||
|
|
||||||
|
let(:resolvable_conflicts) { true }
|
||||||
|
let(:conflict_file) { double(our_path: diff_file_with_conflict.new_path) }
|
||||||
|
let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: resolvable_conflicts) }
|
||||||
|
|
||||||
|
let(:merge_ref_head_diff) { true }
|
||||||
|
let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'conflicts are highlighted' do
|
||||||
|
expect(conflict_file).to receive(:diff_lines_for_serializer)
|
||||||
|
expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer)
|
||||||
|
expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'merge ref head diff is not chosen to be displayed' do
|
||||||
|
let(:merge_ref_head_diff) { false }
|
||||||
|
|
||||||
|
it 'conflicts are not calculated' do
|
||||||
|
expect(MergeRequests::Conflicts::ListService).not_to receive(:new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when conflicts cannot be resolved' do
|
||||||
|
let(:resolvable_conflicts) { false }
|
||||||
|
|
||||||
|
it 'conflicts are not highlighted' do
|
||||||
|
expect(conflict_file).not_to receive(:diff_lines_for_serializer)
|
||||||
|
expect(diff_file_with_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
|
||||||
|
expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue