Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-13 18:09:11 +00:00
parent 37699393e9
commit feb61d56e7
104 changed files with 1938 additions and 646 deletions

View File

@ -55,10 +55,7 @@ Graphql/ResolverType:
- 'app/graphql/resolvers/merge_requests_resolver.rb'
- 'app/graphql/resolvers/project_merge_requests_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/snippets_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/package_file_registries_resolver.rb'

View File

@ -1,3 +1,21 @@
import Vue from 'vue';
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,
},
});
});

View File

@ -0,0 +1,3 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
document.addEventListener('DOMContentLoaded', initProfilePreferences);

View File

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

View File

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

View File

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

View File

@ -29,7 +29,6 @@ export default {
'markdownDocsPath',
'markdownPreviewPath',
'releasesPagePath',
'updateReleaseApiDocsPath',
'release',
'newMilestonePath',
'manageMilestonesPath',

View File

@ -1,14 +1,14 @@
<script>
import { mapState } from 'vuex';
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';
export default {
name: 'TagFieldExisting',
components: { GlFormGroup, GlFormInput, GlSprintf, GlLink, FormFieldContainer },
components: { GlFormGroup, GlFormInput, FormFieldContainer },
computed: {
...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
...mapState('detail', ['release']),
inputId() {
return uniqueId('tag-name-input-');
},
@ -32,19 +32,7 @@ export default {
</form-field-container>
<template #description>
<div :id="helpId" data-testid="tag-name-help">
<gl-sprintf
: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>
{{ __("The tag name can't be changed for an existing release.") }}
</div>
</template>
</gl-form-group>

View File

@ -5,7 +5,6 @@ export default ({
projectPath,
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,
@ -20,7 +19,6 @@ export default ({
projectPath,
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,

View File

@ -83,7 +83,13 @@ export default {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
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 {
image: [imageRenderer],
};

View File

@ -12,9 +12,11 @@ const imageRepository = () => {
.catch(() => flash(__('Something went wrong while inserting your image. Please try again.')));
};
const get = path => images.get(path);
const getAll = () => images;
return { add, getAll };
return { add, get, getAll };
};
export default imageRepository;

View File

@ -4,6 +4,8 @@ const canRender = ({ type }) => type === 'image';
let metadata;
const getCachedContent = basePath => metadata.imageRepository.get(basePath);
const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/');
const extractSourceDirectory = url => {
@ -46,7 +48,11 @@ const generateSourceDirectory = basePath => {
return sourceDir || defaultSourceDir;
};
const resolveFullPath = originalSrc => {
const resolveFullPath = (originalSrc, cachedContent) => {
if (cachedContent) {
return `data:image;base64,${cachedContent}`;
}
if (isAbsolute(originalSrc)) {
return originalSrc;
}
@ -61,20 +67,22 @@ const resolveFullPath = originalSrc => {
const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => {
skipChildren();
const cachedContent = getCachedContent(originalSrc);
return {
type: 'openTag',
tagName: 'img',
selfClose: true,
attributes: {
'data-original-src': !isAbsolute(originalSrc) ? originalSrc : '',
src: resolveFullPath(originalSrc),
'data-original-src': !isAbsolute(originalSrc) || cachedContent ? originalSrc : '',
src: resolveFullPath(originalSrc, cachedContent),
alt: firstChild.literal,
},
};
};
const build = (mounts = [], project, branch, baseUrl) => {
metadata = { mounts, project, branch, baseUrl };
const build = (mounts = [], project, branch, baseUrl, imageRepository) => {
metadata = { mounts, project, branch, baseUrl, imageRepository };
return { canRender, render };
};

View File

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

View File

@ -2,7 +2,6 @@
import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
import { isSafeURL, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IMAGE_TABS } from '../../constants';
import UploadImageTab from './upload_image_tab.vue';
@ -15,7 +14,6 @@ export default {
GlTabs,
GlTab,
},
mixins: [glFeatureFlagMixin()],
props: {
imageRoot: {
type: String,
@ -34,10 +32,10 @@ export default {
},
modalTitle: __('Image details'),
okTitle: __('Insert image'),
urlTabTitle: __('By URL'),
urlTabTitle: __('Link to an image'),
urlLabel: __('Image URL'),
descriptionLabel: __('Description'),
uploadTabTitle: __('Upload file'),
uploadTabTitle: __('Upload an image'),
computed: {
altText() {
return this.description;
@ -54,7 +52,7 @@ export default {
this.$refs.modal.show();
},
onOk(event) {
if (this.glFeatures.sseImageUploads && this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) {
this.submitFile(event);
return;
}
@ -108,7 +106,7 @@ export default {
:ok-title="$options.okTitle"
@ok="onOk"
>
<gl-tabs v-if="glFeatures.sseImageUploads" v-model="tabIndex">
<gl-tabs v-model="tabIndex">
<!-- Upload file Tab -->
<gl-tab :title="$options.uploadTabTitle">
<upload-image-tab ref="uploadImageTab" @input="setFile" />
@ -128,17 +126,6 @@ export default {
</gl-tab>
</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 -->
<gl-form-group :label="$options.descriptionLabel" label-for="description-input">
<gl-form-input id="description-input" ref="descriptionInput" v-model="description" />

View File

@ -114,10 +114,9 @@ export default {
if (file) {
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() {
this.$refs.insertVideoModal.show();

View File

@ -34,6 +34,20 @@ const buildVideoIframe = src => {
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 => {
const { icon, classes, event, command, tooltip, isDivider } = config;
@ -59,7 +73,14 @@ export const addCustomEventListener = (editorApi, event, handler) => {
export const removeCustomEventListener = (editorApi, 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) => {
const videoIframe = buildVideoIframe(url);

View File

@ -31,6 +31,10 @@ module NotesActions
# We know there's more data, so tell the frontend to poll again after 1ms
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)
end
@ -115,7 +119,7 @@ module NotesActions
end
def gather_some_notes
paginator = Gitlab::UpdatedNotesPaginator.new(
paginator = ::Gitlab::UpdatedNotesPaginator.new(
notes_finder.execute.inc_relations_for_view,
last_fetched_at: last_fetched_at
)

View File

@ -34,6 +34,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
environment: environment,
merge_request: @merge_request,
diff_view: diff_view,
merge_ref_head_diff: render_merge_ref_head_diff?,
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) }
}
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?
options[:context_commits] = @merge_request.recent_context_commits
@ -116,7 +120,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
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)
.execute(@project, @merge_request.target_branch)
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)
end
def render_merge_ref_head_diff?
Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref?
end
def note_positions
@note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
end

View File

@ -16,9 +16,6 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:show]
before_action :assign_ref_and_path, 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

View File

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

View File

@ -4,7 +4,7 @@ module ResolvesSnippets
extend ActiveSupport::Concern
included do
type Types::SnippetType, null: false
type Types::SnippetType.connection_type, null: false
argument :ids, [::Types::GlobalIDType[::Snippet]],
required: false,

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Graphql/ResolverType
module Resolvers
module Projects

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Graphql/ResolverType
module Resolvers
class SnippetsResolver < BaseResolver

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Graphql/ResolverType
module Resolvers
module Users

View File

@ -252,4 +252,18 @@ module DiffHelper
"...#{path[-(max - 3)..-1]}"
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

View File

@ -2,9 +2,6 @@
module GitpodHelper
def gitpod_enable_description
link_start = '<a href="https://gitpod.io/" target="_blank" rel="noopener noreferrer">'.html_safe
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 }
s_('Enable %{linkStart}Gitpod%{linkEnd} integration to launch a development environment in your browser directly from GitLab.')
end
end

View File

@ -82,8 +82,8 @@ module PreferencesHelper
def integration_views
[].tap do |views|
views << 'gitpod' if Gitlab::Gitpod.feature_and_settings_enabled?
views << 'sourcegraph' if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_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 << { 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

View File

@ -65,7 +65,6 @@ module ReleasesHelper
project_path: @project.full_path,
markdown_preview_path: preview_markdown_path(@project),
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'),
manage_milestones_path: project_milestones_path(@project),
new_milestone_path: new_project_milestone_path(@project)

View File

@ -2,26 +2,22 @@
module SourcegraphHelper
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 =
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
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
message % { link_start: link_start, link_end: link_end }
end
experimental_message =
if Gitlab::Sourcegraph.feature_conditional?
s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.")
elsif Gitlab::CurrentSettings.sourcegraph_public_only
s_("SourcegraphPreferences|This feature is experimental and limited to public projects.")
else
s_("SourcegraphPreferences|This feature is experimental.")
end
def sourcegraph_experimental_message
if Gitlab::Sourcegraph.feature_conditional?
s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.")
elsif Gitlab::CurrentSettings.sourcegraph_public_only
s_("SourcegraphPreferences|This feature is experimental and limited to public projects.")
else
s_("SourcegraphPreferences|This feature is experimental.")
end
"#{message} #{experimental_message}"
end
end

View File

@ -3,6 +3,7 @@
class DiffFileEntity < DiffFileBaseEntity
include CommitsHelper
include IconsHelper
include Gitlab::Utils::StrongMemoize
expose :added_lines
expose :removed_lines
@ -54,11 +55,16 @@ class DiffFileEntity < DiffFileBaseEntity
# 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|
diff_file.diff_lines_for_serializer
file = conflict_file(options, diff_file) || diff_file
file.diff_lines_for_serializer
end
expose :is_fully_expanded do |diff_file|
diff_file.fully_expanded?
if conflict_file(options, diff_file)
false
else
diff_file.fully_expanded?
end
end
# Used for parallel diffs
@ -79,4 +85,10 @@ class DiffFileEntity < DiffFileBaseEntity
# If nothing is present, inline will be the default.
options.fetch(:diff_view, :inline).to_sym == :inline
end
def conflict_file(options, diff_file)
strong_memoize(:conflict_file) do
options[:conflicts] && options[:conflicts][diff_file.new_path]
end
end
end

View File

@ -71,7 +71,7 @@ class DiffsEntity < Grape::Entity
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
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
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
@ -88,10 +88,6 @@ class DiffsEntity < Grape::Entity
private
def code_navigation_path(diffs)
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
end
def commit_ids
@commit_ids ||= merge_request.recent_commits.map(&:id)
end

View File

@ -7,6 +7,7 @@
#
class PaginatedDiffEntity < Grape::Entity
include RequestAwareEntity
include DiffHelper
expose :diff_files do |diffs, options|
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
@ -15,7 +16,8 @@ class PaginatedDiffEntity < Grape::Entity
diffs.diff_files,
options.merge(
submodule_links: submodule_links,
code_navigation_path: code_navigation_path(diffs)
code_navigation_path: code_navigation_path(diffs),
conflicts: conflicts
)
)
end
@ -41,10 +43,6 @@ class PaginatedDiffEntity < Grape::Entity
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|
define_method method do
pagination_data[method]

View File

@ -17,6 +17,7 @@ module Users
user.accept_pending_invitations! if user.active_for_authentication?
DeviseMailer.user_admin_approval(user).deliver_later
after_approve_hook(user)
success
else
error(user.errors.full_messages.uniq.join('. '))
@ -27,6 +28,10 @@ module Users
attr_reader :current_user
def after_approve_hook(user)
# overridden by EE module
end
def allowed?
can?(current_user, :approve_user)
end
@ -36,3 +41,5 @@ module Users
end
end
end
Users::ApproveService.prepend_if_ee('EE::Users::ApproveService')

View File

@ -8,7 +8,7 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%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')

View File

@ -2,5 +2,9 @@
- add_page_specific_style 'page_bundles/signup'
.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'

View File

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

View File

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

View File

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

View File

@ -1,146 +1,151 @@
- page_title _('Preferences')
- @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|
= 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|
.col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
= s_('Preferences|Navigation theme')
%p
= s_('Preferences|Customize the appearance of the application header and navigation sidebar.')
.col-lg-8.application-theme
.row
- Gitlab::Themes.each do |theme|
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
.preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
= 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
%h4.gl-mt-0
= s_('Preferences|Navigation theme')
%p
= s_('Preferences|Customize the appearance of the application header and navigation sidebar.')
.col-lg-8.application-theme
.row
- Gitlab::Themes.each do |theme|
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
.preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: user_theme_id == theme.id
= theme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar#syntax-highlighting-theme
%h4.gl-mt-0
= s_('Preferences|Syntax highlighting theme')
%p
= s_('Preferences|This setting allows you to customize the appearance of the syntax.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-8.syntax-theme
- Gitlab::ColorSchemes.each do |scheme|
= label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id
= scheme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar#behavior
%h4.gl-mt-0
= s_('Preferences|Behavior')
%p
= s_('Preferences|This setting allows you to customize the behavior of the system layout and default views.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-8
.form-group
= f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
= f.select :layout, layout_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
.form-group
= f.label :dashboard, class: 'label-bold' do
= s_('Preferences|Homepage content')
= f.select :dashboard, dashboard_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on your homepage.')
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
.form-group
= f.label :project_view, class: 'label-bold' do
= s_('Preferences|Project overview content')
= f.select :project_view, project_view_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on a projects overview page.')
.form-group.form-check
= f.check_box :render_whitespace_in_code, class: 'form-check-input'
= f.label :render_whitespace_in_code, class: 'form-check-label' do
= s_('Preferences|Render whitespace characters in the Web IDE')
.form-group.form-check
= f.check_box :show_whitespace_in_diffs, class: 'form-check-input'
= f.label :show_whitespace_in_diffs, class: 'form-check-label' do
= s_('Preferences|Show whitespace changes in diffs')
- if Feature.enabled?(:view_diffs_file_by_file, default_enabled: true)
.form-group.form-check
= f.check_box :view_diffs_file_by_file, class: 'form-check-input'
= f.label :view_diffs_file_by_file, class: 'form-check-label' do
= s_("Preferences|Show one file at a time on merge request's Changes tab")
.form-text.text-muted
= s_("Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser.")
.form-group
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
= f.number_field :tab_width,
class: 'form-control',
min: Gitlab::TabWidth::MIN,
max: Gitlab::TabWidth::MAX,
required: true
.form-text.text-muted
= s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX }
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar#localization
%h4.gl-mt-0
= _('Localization')
%p
= _('Customize language and region related settings.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank'
.col-lg-8
.form-group
= f.label :preferred_language, class: 'label-bold' do
= _('Language')
= f.select :preferred_language, language_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|This feature is experimental and translations are not complete yet')
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
= f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2'
- if Feature.enabled?(:user_time_settings)
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_('Preferences|Time preferences')
%p= s_('Preferences|These settings will update how dates and times are displayed for you.')
.col-lg-4.profile-settings-sidebar#syntax-highlighting-theme
%h4.gl-mt-0
= s_('Preferences|Syntax highlighting theme')
%p
= s_('Preferences|This setting allows you to customize the appearance of the syntax.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-8.syntax-theme
- Gitlab::ColorSchemes.each do |scheme|
= label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id
= scheme.name
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar#behavior
%h4.gl-mt-0
= s_('Preferences|Behavior')
%p
= s_('Preferences|This setting allows you to customize the behavior of the system layout and default views.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-8
.form-group
%h5= s_('Preferences|Time format')
.checkbox-icon-inline-wrapper
- time_format_label = capture do
= s_('Preferences|Display time in 24-hour format')
= f.check_box :time_format_in_24h
= f.label :time_format_in_24h do
= time_format_label
%h5= s_('Preferences|Time display')
.checkbox-icon-inline-wrapper
- time_display_label = capture do
= s_('Preferences|Use relative times')
= f.check_box :time_display_relative
= f.label :time_display_relative do
= time_display_label
= f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
= f.select :layout, layout_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
.form-group
= f.label :dashboard, class: 'label-bold' do
= s_('Preferences|Homepage content')
= f.select :dashboard, dashboard_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on your homepage.')
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
.form-group
= f.label :project_view, class: 'label-bold' do
= s_('Preferences|Project overview content')
= f.select :project_view, project_view_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on a projects overview page.')
.form-group.form-check
= f.check_box :render_whitespace_in_code, class: 'form-check-input'
= f.label :render_whitespace_in_code, class: 'form-check-label' do
= s_('Preferences|Render whitespace characters in the Web IDE')
.form-group.form-check
= f.check_box :show_whitespace_in_diffs, class: 'form-check-input'
= f.label :show_whitespace_in_diffs, class: 'form-check-label' do
= s_('Preferences|Show whitespace changes in diffs')
- if Feature.enabled?(:view_diffs_file_by_file, default_enabled: true)
.form-group.form-check
= f.check_box :view_diffs_file_by_file, class: 'form-check-input'
= f.label :view_diffs_file_by_file, class: 'form-check-label' do
= s_("Preferences|Show one file at a time on merge request's Changes tab")
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
= s_("Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser.")
.form-group
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
= f.number_field :tab_width,
class: 'form-control',
min: Gitlab::TabWidth::MIN,
max: Gitlab::TabWidth::MAX,
required: true
.form-text.text-muted
= s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX }
= render 'integrations', f: f
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
.col-lg-8
.form-group
= f.submit _('Save changes'), class: 'gl-button btn btn-success'
.col-lg-4.profile-settings-sidebar#localization
%h4.gl-mt-0
= _('Localization')
%p
= _('Customize language and region related settings.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank'
.col-lg-8
.form-group
= f.label :preferred_language, class: 'label-bold' do
= _('Language')
= f.select :preferred_language, language_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|This feature is experimental and translations are not complete yet')
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
= f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2'
- if Feature.enabled?(:user_time_settings)
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_('Preferences|Time preferences')
%p= s_('Preferences|These settings will update how dates and times are displayed for you.')
.col-lg-8
.form-group
%h5= s_('Preferences|Time format')
.checkbox-icon-inline-wrapper
- time_format_label = capture do
= s_('Preferences|Display time in 24-hour format')
= f.check_box :time_format_in_24h
= f.label :time_format_in_24h do
= time_format_label
%h5= s_('Preferences|Time display')
.checkbox-icon-inline-wrapper
- time_display_label = capture do
= s_('Preferences|Use relative times')
= f.check_box :time_display_relative
= f.label :time_display_relative do
= time_display_label
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
#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-8
.form-group
= f.submit _('Save changes'), class: 'gl-button btn btn-success'

View File

@ -20,7 +20,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
changes = Base64.decode64(changes) unless changes.include?(' ')
# Use Sidekiq.logger so arguments can be correlated with execution
# 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)
if repo_type.wiki?

View File

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

View File

@ -0,0 +1,5 @@
---
title: Enable the ability to upload images via the SSE
merge_request: 36299
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add type annotation for snippet resolvers
merge_request: 47548
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update the tag name field helper text on the Edit Release page
merge_request: 47234
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Enable Sidekiq argument logging by default
merge_request: 44853
author:
type: changed

View File

@ -1,7 +1,7 @@
---
name: display_merge_conflicts_in_diff
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'
type: development
group: group::source code

View File

@ -1,4 +1,9 @@
# 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?
return true unless Feature::FlipperFeature.table_exists?
@ -35,7 +40,7 @@ Sidekiq.configure_server do |config|
config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
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
}))

View File

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

View File

@ -856,7 +856,9 @@ This file is stored in:
- `/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.
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
{

View File

@ -26,19 +26,11 @@ preventing other threads from continuing.
## Log arguments to Sidekiq jobs
If you want to see what arguments are being passed to Sidekiq jobs you can set
the `SIDEKIQ_LOG_ARGUMENTS` [environment variable](https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true).
Example:
```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.
[In GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44853)
some arguments passed to Sidekiq jobs are logged by default.
To avoid logging sensitive information (for instance, password reset tokens),
GitLab logs numeric arguments for all workers, with overrides for some specific
workers where their arguments are not sensitive.
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
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
Send the Sidekiq process ID the `TTIN` signal and it will output thread

View File

@ -1904,6 +1904,16 @@ input BoardIssueInput {
"""
epicWildcardId: EpicWildcardId
"""
Filter by iteration title
"""
iterationTitle: String
"""
Filter by iteration ID wildcard
"""
iterationWildcardId: IterationWildcardId
"""
Filter by label name
"""
@ -11239,11 +11249,6 @@ enum IssueType {
Represents an iteration object
"""
type Iteration implements TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Timestamp of iteration creation
"""
@ -11371,6 +11376,21 @@ enum IterationState {
upcoming
}
"""
Iteration ID wildcard values
"""
enum IterationWildcardId {
"""
An iteration is assigned
"""
ANY
"""
No iteration is assigned
"""
NONE
}
"""
Represents untyped JSON
"""
@ -13314,11 +13334,6 @@ type MetricsDashboardAnnotationEdge {
Represents a milestone
"""
type Milestone implements TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Timestamp of milestone creation
"""
@ -13883,6 +13898,11 @@ input NegatedBoardIssueInput {
"""
epicId: EpicID
"""
Filter by iteration title
"""
iterationTitle: String
"""
Filter by label name
"""
@ -20948,11 +20968,6 @@ type TimeboxReport {
}
interface TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Historically accurate report about the timebox
"""

View File

@ -5111,6 +5111,16 @@
},
"defaultValue": null
},
{
"name": "iterationTitle",
"description": "Filter by iteration title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "Filter by weight",
@ -5150,6 +5160,16 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "iterationWildcardId",
"description": "Filter by iteration ID wildcard",
"type": {
"kind": "ENUM",
"name": "IterationWildcardId",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
@ -30696,28 +30716,6 @@
"name": "Iteration",
"description": "Represents an iteration object",
"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",
"description": "Timestamp of iteration creation",
@ -31135,6 +31133,29 @@
],
"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",
"name": "JSON",
@ -36633,28 +36654,6 @@
"name": "Milestone",
"description": "Represents a milestone",
"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",
"description": "Timestamp of milestone creation",
@ -41116,6 +41115,16 @@
},
"defaultValue": null
},
{
"name": "iterationTitle",
"description": "Filter by iteration title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "Filter by weight",
@ -60920,28 +60929,6 @@
"name": "TimeboxReportInterface",
"description": null,
"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",
"description": "Historically accurate report about the timebox",

View File

@ -1719,7 +1719,6 @@ Represents an iteration object.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
| `createdAt` | Time! | Timestamp of iteration creation |
| `description` | String | Description of the iteration |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
@ -2042,7 +2041,6 @@ Represents a milestone.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
| `createdAt` | Time! | Timestamp of milestone creation |
| `description` | String | Description of the milestone |
| `dueDate` | Time | Timestamp of the milestone due date |
@ -3994,6 +3992,15 @@ State of a GitLab iteration.
| `started` | |
| `upcoming` | |
### IterationWildcardId
Iteration ID wildcard values.
| Value | Description |
| ----- | ----------- |
| `ANY` | An iteration is assigned |
| `NONE` | No iteration is assigned |
### ListLimitMetric
List limit metric setting.

View File

@ -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 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
Allow to share project with group.

View File

@ -284,11 +284,14 @@ When running your project pipeline at this point:
#### Custom build job for Auto DevOps
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
AWS EC2, you must specify a job for the `build` stage.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6.
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:
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
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
# .gitlab-ci.yml

View File

@ -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.
- `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:
- Key should expire in 29 days for daily and 42 days for weekly.

View File

@ -696,8 +696,8 @@ blocks:
## Arguments logging
When [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs)
is enabled, Sidekiq job arguments will be logged.
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 disabled.
By default, the only arguments logged are numeric arguments, because
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

View File

@ -5,50 +5,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
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.
Merge request approvals rules prevent users overriding certain settings on a project
level. When configured, only administrators can change these settings on a project level
if they are enabled at an instance level.
Merge request approval rules prevent users from overriding certain settings on the project
level. When enabled at the instance level, these settings are no longer editable on the
project level.
To enable merge request approval rules for an instance:
1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge
requests approvals**.
requests approvals**.
1. Set the required rule.
1. Click **Save changes**.
GitLab administrators can later override these settings in a projects settings.
## Available rules
Merge request approval rules that can be set at an instance level are:
- **Prevent approval of merge requests by merge request author**. Prevents project
maintainers from allowing request authors to merge their own merge requests.
maintainers from allowing request authors to merge their own merge requests.
- **Prevent approval of merge requests by merge request committers**. Prevents project
maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch.
- **Can override approvers and approvals required per merge request**. Allows project
maintainers to modify the approvers list 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) |
maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch.
- **Prevent users from modifying merge request approvers list**. Prevents users from
modifying the approvers list in project settings or in individual merge requests.

View File

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

View File

@ -432,7 +432,7 @@ and the following environment variables:
| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` |
| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` |
| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` |
| `SIDEKIQ_LOG_ARGUMENTS` | `1` | - |
| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` |
NOTE: **Note:**
The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import

View File

@ -240,6 +240,14 @@ For users without permissions to view the project's code:
- The wiki homepage is displayed, if any.
- 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
When a repository path changes, it is essential to smoothly transition from the

View File

@ -107,13 +107,36 @@ The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing t
### Images
> - 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}**).
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
#### Upload an image
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
so you can verify the correct image is included and there aren't any references to missing images.
default directory (`source/images/`).
### Videos

View File

@ -9,6 +9,11 @@ module Gitlab
CONTEXT_LINES = 3
CONFLICT_TYPES = {
"old" => "conflict_marker_their",
"new" => "conflict_marker_our"
}.freeze
attr_reader :merge_request
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
@ -46,6 +51,34 @@ module Gitlab
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
return @sections if @sections
@ -93,9 +126,15 @@ module Gitlab
lines = tail_lines
elsif conflict_before
# We're at the end of the file (no conflicts after), so just remove extra
# trailing lines.
# We're at the end of the file (no conflicts after)
number_of_trailing_lines = lines.size
# Remove extra trailing lines
lines = lines.first(CONTEXT_LINES)
if number_of_trailing_lines > CONTEXT_LINES
lines << create_match_line(lines.last)
end
end
end
@ -117,6 +156,10 @@ module Gitlab
Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
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
# match line header. Only context sections can contain match lines, as match lines
# have to exist in both versions of the file.

View File

@ -8,9 +8,9 @@ module Gitlab
#
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_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)
@text, @type, @index = text, type, index

View File

@ -3,6 +3,14 @@
module Gitlab
module EtagCaching
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)
@app = app
end
@ -22,9 +30,7 @@ module Gitlab
else
track_cache_miss(if_none_match, cached_value_present, route)
status, headers, body = @app.call(env)
headers['ETag'] = etag
[status, headers, body]
maybe_apply_etag(etag, *@app.call(env))
end
end
@ -43,6 +49,13 @@ module Gitlab
[weak_etag_format(current_value), cached_value_present]
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)
%Q{W/"#{value}"}
end

View File

@ -4,6 +4,8 @@ module Gitlab
module Graphql
module Present
class Instrumentation
SAFE_CONTEXT_KEYS = %i[current_user].freeze
def instrument(type, field)
return field unless field.metadata[:type_class]
@ -22,7 +24,8 @@ module Gitlab
next old_resolver.call(presented_type, args, context)
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
wrapped = presented_type.class.authorized_new(presenter, context)
@ -34,6 +37,12 @@ module Gitlab
resolve(resolve_with_presenter)
end
end
private
def safe_context_values(context)
context.to_h.slice(*SAFE_CONTEXT_KEYS)
end
end
end
end

View File

@ -32,20 +32,20 @@ module Gitlab
end
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
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
def self.http_request_duration_seconds
@http_request_duration_seconds ||= ::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])
::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])
end
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
def self.initialize_metrics

View File

@ -16,7 +16,7 @@ module Gitlab
# Add process id params
job['pid'] = ::Process.pid
job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS']
job.delete('args') unless SidekiqLogArguments.enabled?
job
end

View File

@ -1,42 +1,8 @@
# frozen_string_literal: true
module Gitlab::UsageDataCounters
class DesignsCounter
extend Gitlab::UsageDataCounters::RedisCounter
class DesignsCounter < BaseCounter
KNOWN_EVENTS = %w[create update delete].freeze
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
PREFIX = 'design_management_designs'
end
end

View File

@ -2,54 +2,43 @@
module Gitlab
module UsageDataCounters
class WebIdeCounter
extend RedisCounter
KNOWN_EVENTS = %i[commits views merge_requests previews terminals pipelines].freeze
class WebIdeCounter < BaseCounter
KNOWN_EVENTS = %w[commits views merge_requests previews terminals pipelines].freeze
PREFIX = 'web_ide'
class << self
def increment_commits_count
increment(redis_key('commits'))
count('commits')
end
def increment_merge_requests_count
increment(redis_key('merge_requests'))
count('merge_requests')
end
def increment_views_count
increment(redis_key('views'))
count('views')
end
def increment_terminals_count
increment(redis_key('terminals'))
count('terminals')
end
def increment_pipelines_count
increment(redis_key('pipelines'))
count('pipelines')
end
def increment_previews_count
return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
increment(redis_key('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
count('previews')
end
private
def redis_key(event)
"#{PREFIX}_#{event}_count".upcase
end
require_known_event(event)
def counter_key(event)
"#{PREFIX}_#{event}".to_sym
"#{prefix}_#{event}_count".upcase
end
end
end

View File

@ -4696,9 +4696,6 @@ msgstr ""
msgid "By %{user_name}"
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}"
msgstr ""
@ -5053,9 +5050,6 @@ msgstr ""
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
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."
msgstr ""
@ -6987,9 +6981,6 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
msgid "Compliance frameworks"
msgstr ""
msgid "ComplianceDashboard|created by:"
msgstr ""
@ -10113,7 +10104,7 @@ msgstr ""
msgid "Enable"
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 ""
msgid "Enable Auto DevOps"
@ -16149,6 +16140,9 @@ msgstr ""
msgid "Link title is required"
msgstr ""
msgid "Link to an image"
msgstr ""
msgid "Link to go to GitLab pipeline documentation"
msgstr ""
@ -20313,18 +20307,12 @@ msgstr ""
msgid "Preferences|Choose what content you want to see on your homepage."
msgstr ""
msgid "Preferences|Customize integrations with third party services."
msgstr ""
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
msgstr ""
msgid "Preferences|Display time in 24-hour format"
msgstr ""
msgid "Preferences|Enable integrated code intelligence on code views"
msgstr ""
msgid "Preferences|For example: 30 mins ago."
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."
msgstr ""
msgid "Preferences|Integrations"
msgstr ""
msgid "Preferences|Layout width"
msgstr ""
@ -20358,9 +20343,6 @@ msgstr ""
msgid "Preferences|Show whitespace changes in diffs"
msgstr ""
msgid "Preferences|Sourcegraph"
msgstr ""
msgid "Preferences|Syntax highlighting theme"
msgstr ""
@ -20415,6 +20397,9 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
msgstr ""
@ -20544,6 +20529,24 @@ msgstr ""
msgid "Profile Settings"
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"
msgstr ""
@ -22331,9 +22334,6 @@ msgstr ""
msgid "Registry setup"
msgstr ""
msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
msgstr ""
msgid "Reindexing status"
msgstr ""
@ -25593,10 +25593,10 @@ msgstr ""
msgid "SourcegraphPreferences|This feature is experimental."
msgstr ""
msgid "SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}."
msgid "SourcegraphPreferences|Uses %{linkStart}Sourcegraph.com%{linkEnd}."
msgstr ""
msgid "SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}."
msgid "SourcegraphPreferences|Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}."
msgstr ""
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."
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."
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."
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."
msgstr ""
@ -29180,6 +29180,9 @@ msgstr ""
msgid "Upload a private key for your certificate"
msgstr ""
msgid "Upload an image"
msgstr ""
msgid "Upload file"
msgstr ""

View File

@ -383,6 +383,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
environment: nil,
merge_request: merge_request,
diff_view: :inline,
merge_ref_head_diff: nil,
pagination_data: {
current_page: nil,
next_page: nil,

View File

@ -113,6 +113,8 @@ RSpec.describe Projects::NotesController do
end
it 'returns the first page of notes' do
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
get :index, params: request_params
expect(json_response['notes'].count).to eq(page_1.count)
@ -122,6 +124,8 @@ RSpec.describe Projects::NotesController do
end
it 'returns the second page of notes' do
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
request.headers['X-Last-Fetched-At'] = page_1_boundary
get :index, params: request_params
@ -133,6 +137,8 @@ RSpec.describe Projects::NotesController do
end
it 'returns the final page of notes' do
expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
request.headers['X-Last-Fetched-At'] = page_2_boundary
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(response.headers['Poll-Interval'].to_i).to be > 1
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
context 'feature flag disabled' do

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ describe('Release edit/new component', () => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page',
projectId: '8',
groupId: '42',

View File

@ -6,7 +6,6 @@ import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_DOCS_PATH = '/help/test/docs/path';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -24,21 +23,11 @@ describe('releases/components/tag_field_existing', () => {
const findInput = () => wrapper.find(GlFormInput);
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(() => {
store = createStore({
modules: {
detail: createDetailModule({
updateReleaseApiDocsPath: TEST_DOCS_PATH,
tagName: TEST_TAG_NAME,
}),
},
@ -68,16 +57,8 @@ describe('releases/components/tag_field_existing', () => {
createComponent(mount);
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',
});
});
});
});

View File

@ -47,7 +47,6 @@ describe('Release detail actions', () => {
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
}),
...getters,
...rootState,

View File

@ -18,7 +18,6 @@ describe('Release detail mutations', () => {
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
});
release = convertObjectPropsToCamelCase(originalRelease);
});

View File

@ -3,9 +3,11 @@ import { mounts, project, branch, baseUrl } from '../../mock_data';
describe('rich_content_editor/renderers/render_image', () => {
let renderer;
let imageRepository;
beforeEach(() => {
renderer = imageRenderer.build(mounts, project, branch, baseUrl);
renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository);
imageRepository = { get: () => null };
});
describe('build', () => {
@ -27,6 +29,21 @@ describe('rich_content_editor/renderers/render_image', () => {
});
describe('render', () => {
let skipChildren;
let context;
let node;
beforeEach(() => {
skipChildren = jest.fn();
context = { skipChildren };
node = {
firstChild: {
type: 'img',
literal: 'Some Image',
},
};
});
it.each`
destination | isAbsolute | src
${'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'}
`('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => {
const skipChildren = jest.fn();
const context = { skipChildren };
const node = {
destination,
firstChild: {
type: 'img',
literal: 'Some Image',
},
};
node.destination = destination;
const result = renderer.render(node, context);
expect(result).toEqual({
@ -60,5 +70,27 @@ describe('rich_content_editor/renderers/render_image', () => {
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',
},
});
});
});
});

View File

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

View File

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

View File

@ -91,12 +91,25 @@ describe('Editor Service', () => {
});
describe('addImage', () => {
it('calls the exec method on the instance', () => {
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
const file = new File([], 'some-file.jpg');
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)');
});
});

View File

@ -15,10 +15,7 @@ describe('Add Image Modal', () => {
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
beforeEach(() => {
wrapper = shallowMount(AddImageModal, {
provide: { glFeatures: { sseImageUploads: true } },
propsData,
});
wrapper = shallowMount(AddImageModal, { propsData });
});
describe('when content is loaded', () => {

View File

@ -180,7 +180,7 @@ describe('Rich Content Editor', () => {
wrapper.vm.$refs.editor = mockInstance;
findAddImageModal().vm.$emit('addImage', mockImage);
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined);
});
});

View File

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

View File

@ -71,7 +71,6 @@ RSpec.describe ReleasesHelper do
markdown_preview_path
markdown_docs_path
releases_page_path
update_release_api_docs_path
release_assets_docs_path
manage_milestones_path
new_milestone_path)
@ -89,7 +88,6 @@ RSpec.describe ReleasesHelper do
releases_page_path
markdown_preview_path
markdown_docs_path
update_release_api_docs_path
release_assets_docs_path
manage_milestones_path
new_milestone_path

View File

@ -5,60 +5,43 @@ require 'spec_helper'
RSpec.describe SourcegraphHelper do
describe '#sourcegraph_url_message' do
let(:sourcegraph_url) { 'http://sourcegraph.example.com' }
let(:feature_conditional) { false }
let(:public_only) { false }
let(:is_com) { true }
before do
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_public_only).and_return(public_only)
allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional)
end
subject { helper.sourcegraph_url_message }
context 'with .com sourcegraph url' do
let(:is_com) { true }
it { is_expected.to have_text('Uses Sourcegraph.com') }
it { is_expected.to have_link('Sourcegraph.com', href: sourcegraph_url) }
it { is_expected.to have_text('Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.') }
end
context 'with custom sourcegraph url' do
let(:is_com) { false }
it { is_expected.to have_text('Uses a custom Sourcegraph instance') }
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
it { is_expected.to have_text('Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}. This feature is experimental.') }
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
it { is_expected.to eq "This feature is experimental." }
it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.' }
end
context 'when limited by feature' do
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
context 'when limited by public only' do
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

View File

@ -93,6 +93,51 @@ RSpec.describe Gitlab::Conflict::File do
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
it 'only inserts match lines when there is a gap between sections' do
conflict_file.sections.each_with_index do |section, i|

View File

@ -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(: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
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
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|
let(:expected_items) do
{

View File

@ -127,7 +127,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
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
expected_labels = []

View File

@ -119,6 +119,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
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
Timecop.freeze(timestamp) do
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
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
expect(logger).to receive(:info).with(end_payload.except('args')).ordered
expect(logger).to receive(:info).with(start_payload).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_done).and_call_original
@ -173,12 +177,12 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
let(:expected_end_payload) do
end_payload.except('args').merge(timing_data)
end_payload.merge(timing_data)
end
it 'logs with Gitaly and Rugged timing data' 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
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
end
let(:expected_start_payload) { start_payload.except('args') }
let(:expected_start_payload) { start_payload }
let(:expected_end_payload) do
end_payload.except('args').merge('cpu_s' => a_value >= 0)
end_payload.merge('cpu_s' => a_value >= 0)
end
let(:expected_end_payload_with_db) do
@ -228,10 +232,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
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
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
it 'logs it in the done log' do

View File

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

View File

@ -69,4 +69,15 @@ RSpec.describe DiffFileEntity do
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

View File

@ -8,9 +8,12 @@ RSpec.describe DiffsEntity do
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_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
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
context 'as json' do
@ -68,5 +71,50 @@ RSpec.describe DiffsEntity do
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

Some files were not shown because too many files have changed in this diff Show More