Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-12 15:08:32 +00:00
parent b47e7cd6b2
commit b17f0b91a6
125 changed files with 1345 additions and 1152 deletions

View File

@ -94,7 +94,8 @@
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:4.0-alpine
- name: elasticsearch:6.4.2
- name: elasticsearch:7.9.2
command: ["elasticsearch", "-E", "discovery.type=single-node"]
variables:
POSTGRES_HOST_AUTH_METHOD: trust
@ -104,7 +105,8 @@
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:4.0-alpine
- name: elasticsearch:6.4.2
- name: elasticsearch:7.9.2
command: ["elasticsearch", "-E", "discovery.type=single-node"]
variables:
POSTGRES_HOST_AUTH_METHOD: trust

View File

@ -1165,11 +1165,6 @@ Rails/SaveBang:
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
- 'spec/support/migrations_helpers/cluster_helpers.rb'
- 'spec/support/migrations_helpers/namespaces_helper.rb'
- 'spec/support/shared_contexts/email_shared_context.rb'
- 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb'
- 'spec/support/shared_contexts/mailers/notify_shared_context.rb'
# Offense count: 187
# Cop supports --auto-correct.

View File

@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
gem 'rack-proxy', '~> 0.6.0'
gem 'sassc-rails', '~> 2.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'terser', '~> 1.0'
gem 'addressable', '~> 2.7'
gem 'font-awesome-rails', '~> 4.7'
@ -430,7 +430,7 @@ end
gem 'octokit', '~> 4.15'
# https://gitlab.com/gitlab-org/gitlab/issues/207207
gem 'gitlab-mail_room', '~> 0.0.6', require: 'mail_room'
gem 'gitlab-mail_room', '~> 0.0.7', require: 'mail_room'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'

View File

@ -312,7 +312,7 @@ GEM
tzinfo
eventmachine (1.2.7)
excon (0.71.1)
execjs (2.6.0)
execjs (2.7.0)
expression_parser (0.9.0)
extended-markdown-filter (0.6.0)
html-pipeline (~> 2.0)
@ -436,7 +436,7 @@ GEM
opentracing (~> 0.4)
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.0.0)
gitlab-mail_room (0.0.6)
gitlab-mail_room (0.0.7)
gitlab-markup (1.7.1)
gitlab-net-dns (0.9.1)
gitlab-puma (4.3.5.gitlab.3)
@ -1130,6 +1130,8 @@ GEM
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terser (1.0.1)
execjs (>= 0.3.0, < 3)
test-prof (0.12.0)
text (1.3.1)
thin (1.7.2)
@ -1157,9 +1159,6 @@ GEM
thread_safe (~> 0.1)
u2f (0.2.1)
uber (0.1.0)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
@ -1327,7 +1326,7 @@ DEPENDENCIES
gitlab-fog-azure-rm (~> 1.0)
gitlab-labkit (= 0.12.1)
gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.6)
gitlab-mail_room (~> 0.0.7)
gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)
gitlab-puma (~> 4.3.3.gitlab.2)
@ -1483,13 +1482,13 @@ DEPENDENCIES
stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
terser (~> 1.0)
test-prof (~> 0.12.0)
thin (~> 1.7.0)
timecop (~> 0.9.1)
toml-rb (~> 1.0.0)
truncato (~> 0.7.11)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
unf (~> 0.1.4)
unicorn (~> 5.5)
unicorn-worker-killer (~> 0.4.4)

View File

@ -0,0 +1,96 @@
import { flatten } from 'lodash';
import { s__ } from '~/locale';
import AccessorUtilities from '~/lib/utils/accessor';
import { shouldDisableShortcuts } from './shortcuts_toggle';
export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations';
let parsedCustomizations = {};
const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
if (localStorageIsSafe) {
try {
parsedCustomizations = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}');
} catch (e) {
/* do nothing */
}
}
/**
* A map of command => keys of all keyboard shortcuts
* that have been customized by the user.
*
* @example
* { "globalShortcuts.togglePerformanceBar": ["p e r f"] }
*
* @type { Object.<string, string[]> }
*/
export const customizations = parsedCustomizations;
// All available commands
export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar';
/** All keybindings, grouped and ordered with descriptions */
export const keybindingGroups = [
{
groupId: 'globalShortcuts',
name: s__('KeyboardShortcuts|Global Shortcuts'),
keybindings: [
{
description: s__('KeyboardShortcuts|Toggle the Performance Bar'),
command: TOGGLE_PERFORMANCE_BAR,
// eslint-disable-next-line @gitlab/require-i18n-strings
defaultKeys: ['p b'],
},
],
},
]
// For each keybinding object, add a `customKeys` property populated with the
// user's custom keybindings (if the command has been customized).
// `customKeys` will be `undefined` if the command hasn't been customized.
.map(group => {
return {
...group,
keybindings: group.keybindings.map(binding => ({
...binding,
customKeys: customizations[binding.command],
})),
};
});
/**
* A simple map of command => keys. All user customizations are included in this map.
* This mapping is used to simplify `keysFor` below.
*
* @example
* { "globalShortcuts.togglePerformanceBar": ["p e r f"] }
*/
const commandToKeys = flatten(keybindingGroups.map(group => group.keybindings)).reduce(
(acc, binding) => {
acc[binding.command] = binding.customKeys || binding.defaultKeys;
return acc;
},
{},
);
/**
* Gets keyboard shortcuts associated with a command
*
* @param {string} command The command string. All command
* strings are available as imports from this file.
*
* @returns {string[]} An array of keyboard shortcut strings bound to the command
*
* @example
* import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings'
*
* Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), handler);
*/
export const keysFor = command => {
if (shouldDisableShortcuts()) {
return [];
}
return commandToKeys[command];
};

View File

@ -9,6 +9,7 @@ import axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
import { keysFor, TOGGLE_PERFORMANCE_BAR } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
@ -70,7 +71,7 @@ export default class Shortcuts {
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('/', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar);
const findFileURL = document.body.dataset.findFile;

View File

@ -132,7 +132,13 @@ export default {
>
<div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<gl-icon :name="icon.name" :size="18" :class="icon.classes" />
<gl-icon
:name="icon.name"
:size="18"
:class="icon.classes"
data-qa-selector="design_status_icon"
:data-qa-status="icon.name"
/>
</span>
</div>
<gl-intersection-observer @appear="onAppear">

View File

@ -1,4 +1,3 @@
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import {
MATCH_LINE_TYPE,
@ -23,21 +22,8 @@ export const isMatchLine = type => type === MATCH_LINE_TYPE;
export const isMetaLine = type =>
[OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE].includes(type);
export const shouldRenderCommentButton = (
isLoggedIn,
isCommentButtonRendered,
featureMergeRefHeadComments = false,
) => {
if (!isCommentButtonRendered) {
return false;
}
if (isLoggedIn) {
const isDiffHead = parseBoolean(getParameterByName('diff_head'));
return !isDiffHead || featureMergeRefHeadComments;
}
return false;
export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) => {
return isCommentButtonRendered && isLoggedIn;
};
export const hasDiscussions = line => line?.discussions?.length > 0;

View File

@ -81,11 +81,7 @@ export default {
return utils.addCommentTooltip(this.line);
},
shouldRenderCommentButton() {
return utils.shouldRenderCommentButton(
this.isLoggedIn,
true,
gon.features?.mergeRefHeadComments,
);
return utils.shouldRenderCommentButton(this.isLoggedIn, true);
},
shouldShowCommentButton() {
return utils.shouldShowCommentButton(

View File

@ -102,11 +102,7 @@ export default {
return utils.addCommentTooltip(this.line.right);
},
shouldRenderCommentButton() {
return utils.shouldRenderCommentButton(
this.isLoggedIn,
this.isCommentButtonRendered,
gon.features?.mergeRefHeadComments,
);
return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered);
},
shouldShowCommentButtonLeft() {
return utils.shouldShowCommentButton(

View File

@ -1,6 +1,6 @@
import { __ } from '~/locale';
export default IssuableTokenKeys => {
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const draftToken = {
token: {
formattedKey: __('Draft'),
@ -51,18 +51,20 @@ export default IssuableTokenKeys => {
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
IssuableTokenKeys.conditions.push(...draftToken.conditions);
const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: 'target-branch',
type: 'string',
param: '',
symbol: '',
icon: 'arrow-right',
tag: 'branch',
};
if (!disableTargetBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: 'target-branch',
type: 'string',
param: '',
symbol: '',
icon: 'arrow-right',
tag: 'branch',
};
IssuableTokenKeys.tokenKeys.push(targetBranchToken);
IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
IssuableTokenKeys.tokenKeys.push(targetBranchToken);
IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
}
const approvedBy = {
token: {

View File

@ -0,0 +1,120 @@
<script>
import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlIcon,
GlButton,
GlAvatarLink,
GlAvatarLabeled,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
createdAt: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
statusBadgeClass: {
type: String,
required: false,
default: '',
},
statusIcon: {
type: String,
required: false,
default: '',
},
blocked: {
type: Boolean,
required: false,
default: false,
},
confidential: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
},
mounted() {
this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
},
methods: {
handleRightSidebarToggleClick() {
if (this.toggleSidebarButtonEl) {
this.toggleSidebarButtonEl.dispatchEvent(new Event('click'));
}
},
},
};
</script>
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
<div data-testid="status" class="issuable-status-box status-box" :class="statusBadgeClass">
<gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
<span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
</div>
<div class="issuable-meta gl-display-flex gl-align-items-center">
<div class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
<gl-icon name="lock" :aria-label="__('Blocked')" />
</div>
<div v-if="confidential" data-testid="confidential" class="issuable-warning-icon inline">
<gl-icon name="eye-slash" :aria-label="__('Confidential')" />
</div>
</div>
<span>
{{ __('Opened') }}
<time-ago-tooltip data-testid="startTimeItem" :time="createdAt" />
{{ __('by') }}
</span>
<gl-avatar-link
data-testid="avatar"
:data-user-id="authorId"
:data-username="author.username"
:data-name="author.name"
:href="author.webUrl"
target="_blank"
class="js-user-link gl-ml-2"
>
<gl-avatar-labeled
:size="24"
:src="author.avatarUrl"
:label="author.name"
class="d-none d-sm-inline-flex gl-ml-1"
/>
<strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
</gl-avatar-link>
</div>
<gl-button
data-testid="sidebar-toggle"
icon="chevron-double-lg-left"
class="d-block d-sm-none gutter-toggle issuable-gutter-toggle"
:aria-label="__('Expand sidebar')"
@click="handleRightSidebarToggleClick"
/>
</div>
<div
data-testid="header-actions"
class="detail-page-header-actions js-issuable-actions js-issuable-buttons gl-display-flex gl-display-md-block"
>
<slot name="header-actions"></slot>
</div>
</div>
</template>

View File

@ -85,6 +85,21 @@ export const getDayName = date =>
__('Saturday'),
][date.getDay()];
/**
* Returns the i18n month name from a given date
* @example
* formatDateAsMonth(new Date('2020-06-28')) -> 'Jun'
* @param {String} datetime where month is extracted from
* @param {Object} options
* @param {Boolean} options.abbreviated whether to use the abbreviated month string, or not
* @return {String} the i18n month name
*/
export function formatDateAsMonth(datetime, options = {}) {
const { abbreviated = true } = options;
const month = new Date(datetime).getMonth();
return getMonthNames(abbreviated)[month];
}
/**
* @example
* dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"

View File

@ -5,7 +5,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,

View File

@ -38,6 +38,14 @@
top: $mr-file-header-top;
z-index: 120;
.with-system-header & {
top: $mr-file-header-top + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $mr-file-header-top + $system-header-height + $performance-bar-height;
}
&::before {
content: '';
position: absolute;
@ -1078,6 +1086,14 @@ table.code {
max-height: calc(100vh - #{$top-pos});
z-index: 202;
.with-system-header & {
top: $top-pos + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $top-pos + $system-header-height + $performance-bar-height;
}
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;

View File

@ -771,6 +771,14 @@ $mr-widget-min-height: 69px;
position: sticky;
top: $header-height + $mr-tabs-height;
.with-system-header & {
top: $header-height + $mr-tabs-height + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $header-height + $mr-tabs-height + $system-header-height + $performance-bar-height;
}
.mr-version-menus-container {
flex-wrap: nowrap;
}
@ -788,6 +796,14 @@ $mr-widget-min-height: 69px;
background-color: $white;
border-bottom: 1px solid $border-color;
.with-system-header & {
top: $header-height + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $header-height + $system-header-height + $performance-bar-height;
}
@include media-breakpoint-up(sm) {
position: -webkit-sticky;
position: sticky;

View File

@ -17,13 +17,7 @@ module SnippetsActions
respond_to :html
end
def edit
# We need to load some info from the existing blob
snippet.content = blob.data
snippet.file_name = blob.path
render 'edit'
end
def edit; end
# This endpoint is being replaced by Snippets::BlobController#raw
# Support for old raw links will be maintainted via this action but
@ -55,7 +49,6 @@ module SnippetsActions
def show
respond_to do |format|
format.html do
conditionally_expand_blob(blob)
@note = Note.new(noteable: @snippet, project: @snippet.project)
@noteable = @snippet
@ -80,29 +73,6 @@ module SnippetsActions
end
end
end
def update
update_params = snippet_params.merge(spammable_params)
service_response = Snippets::UpdateService.new(@snippet.project, current_user, update_params).execute(@snippet)
@snippet = service_response.payload[:snippet]
handle_repository_error(:edit)
end
def destroy
service_response = Snippets::DestroyService.new(current_user, @snippet).execute
if service_response.success?
redirect_to gitlab_dashboard_snippets_path(@snippet), status: :found
elsif service_response.http_status == 403
access_denied!
else
redirect_to gitlab_snippet_path(@snippet),
status: :found,
alert: service_response.message
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
private
@ -124,12 +94,4 @@ module SnippetsActions
def convert_line_endings(content)
params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
end
def handle_repository_error(action)
errors = Array(snippet.errors.delete(:repository))
flash.now[:alert] = errors.first if errors.present?
recaptcha_check_with_fallback(errors.empty?) { render action }
end
end

View File

@ -173,7 +173,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
def update_diff_discussion_positions!
return unless Feature.enabled?(:merge_ref_head_comments, @merge_request.target_project, default_enabled: true)
return unless Feature.enabled?(:merge_red_head_comments_position_on_demand, @merge_request.target_project, default_enabled: true)
return if @merge_request.has_any_diff_note_positions?

View File

@ -29,7 +29,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_experiment(:suggest_pipeline)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
push_frontend_feature_flag(:file_identifier_hash)

View File

@ -7,12 +7,11 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam]
before_action :authorize_create_snippet!, only: [:new, :create]
before_action :authorize_read_snippet!, except: [:new, :create, :index]
before_action :authorize_update_snippet!, only: [:edit, :update]
before_action :authorize_admin_snippet!, only: [:destroy]
before_action :authorize_create_snippet!, only: :new
before_action :authorize_read_snippet!, except: [:new, :index]
before_action :authorize_update_snippet!, only: :edit
def index
@snippet_counts = ::Snippets::CountService
@ -33,14 +32,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
@snippet = @noteable = @project.snippets.build
end
def create
create_params = snippet_params.merge(spammable_params)
service_response = ::Snippets::CreateService.new(project, current_user, create_params).execute
@snippet = service_response.payload[:snippet]
handle_repository_error(:new)
end
protected
alias_method :awardable, :snippet
@ -49,8 +40,4 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
def spammable_path
project_snippet_path(@project, @snippet)
end
def snippet_params
params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description)
end
end

View File

@ -6,12 +6,11 @@ class SnippetsController < Snippets::ApplicationController
include ToggleAwardEmoji
include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam]
before_action :authorize_create_snippet!, only: [:new, :create]
before_action :authorize_create_snippet!, only: :new
before_action :authorize_read_snippet!, only: [:show, :raw]
before_action :authorize_update_snippet!, only: [:edit, :update]
before_action :authorize_admin_snippet!, only: [:destroy]
before_action :authorize_update_snippet!, only: :edit
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
@ -40,18 +39,6 @@ class SnippetsController < Snippets::ApplicationController
@snippet = PersonalSnippet.new
end
def create
create_params = snippet_params.merge(files: params.delete(:files))
service_response = Snippets::CreateService.new(nil, current_user, create_params).execute
@snippet = service_response.payload[:snippet]
if service_response.error? && @snippet.errors[:repository].present?
handle_repository_error(:new)
else
recaptcha_check_with_fallback { render :new }
end
end
protected
alias_method :awardable, :snippet
@ -60,8 +47,4 @@ class SnippetsController < Snippets::ApplicationController
def spammable_path
snippet_path(@snippet)
end
def snippet_params
params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description).merge(spammable_params)
end
end

View File

@ -24,4 +24,9 @@ module ContainerExpirationPoliciesHelper
end
end
end
def container_expiration_policies_historic_entry_enabled?(project)
Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries ||
Feature.enabled?(:container_expiration_policies_historic_entry, project)
end
end

View File

@ -526,7 +526,6 @@ module Ci
.concat(job_jwt_variables)
.concat(scoped_variables)
.concat(job_variables)
.concat(environment_changed_page_variables)
.concat(persisted_environment_variables)
.to_runner_variables
end
@ -563,15 +562,6 @@ module Ci
end
end
def environment_changed_page_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project)
variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(','))
variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(','))
end
end
def deploy_token_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless gitlab_deploy_token

View File

@ -72,14 +72,6 @@ class EnvironmentStatus
.merge_request_diff_files.where(deleted_file: false)
end
def changed_paths
changes.map { |change| change[:path] }
end
def changed_urls
changes.map { |change| change[:external_url] }
end
def has_route_map?
project.route_map_for(sha).present?
end

View File

@ -69,9 +69,6 @@ class DiscussionEntity < Grape::Entity
end
def display_merge_ref_discussions?(discussion)
return unless discussion.diff_discussion?
return if discussion.legacy_diff_discussion?
Feature.enabled?(:merge_ref_head_comments, discussion.project, default_enabled: true)
discussion.diff_discussion? && !discussion.legacy_diff_discussion?
end
end

View File

@ -125,8 +125,6 @@ module MergeRequests
end
def update_diff_discussion_positions!
return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project, default_enabled: true)
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
end

View File

@ -70,7 +70,7 @@ module Notes
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end
if Feature.enabled?(:merge_ref_head_comments, project, default_enabled: true) && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
if note.for_merge_request? && note.diff_note? && note.start_of_discussion?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
end

View File

@ -14,7 +14,7 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set
= render 'shared/issuable/search_bar', type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests, disable_target_branch: true
- if current_user && @no_filters_set
= render 'shared/dashboard/no_filter_selected'

View File

@ -1,14 +1,14 @@
= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
- if params[:to] && params[:from]
.compare-switch-container
= link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
= link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn gl-button btn-white', title: 'Swap revisions'
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-prepend
.input-group-text
= s_("CompareBranches|Source")
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
= button_tag type: 'button', title: params[:to], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag")
= sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
@ -19,12 +19,12 @@
.input-group-text
= s_("CompareBranches|Target")
= hidden_field_tag :from, params[:from]
= button_tag type: 'button', title: params[:from], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
= button_tag type: 'button', title: params[:from], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag")
= sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
&nbsp;
= button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn"
= button_tag s_("CompareBranches|Compare"), class: "btn gl-button btn-success commits-compare-btn"
- if @merge_request.present?
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'gl-ml-3 btn'
- elsif create_mr_button?

View File

@ -5,4 +5,4 @@
older_than_options: older_than_options.to_json,
is_admin: current_user&.admin.to_s,
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
enable_historic_entries: Gitlab::CurrentSettings.try(:container_expiration_policies_enable_historic_entries).to_s} }
enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s} }

View File

@ -1,6 +1,7 @@
- type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil)
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
@ -154,11 +155,12 @@
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
- unless disable_target_branch
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
= render_if_exists 'shared/issuable/filter_weight', type: type

View File

@ -100,7 +100,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
end
def flush_ref_caches(project)
project.repository.after_create_branch
project.repository.expire_branches_cache
project.repository.branch_names
project.repository.has_visible_content?
end

View File

@ -0,0 +1,5 @@
---
title: Apply GitLab UI button styles to buttons in app/views/projects/compare directory
merge_request: 44342
author: Lakshit
type: other

View File

@ -0,0 +1,5 @@
---
title: Add cache:when keyword for ci yml config
merge_request: 41822
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add feature flag for a phased rollout of cleanup policies
merge_request: 44444
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses in spec/support/*
merge_request: 44884
author: matthewbried
type: other

View File

@ -0,0 +1,5 @@
---
title: Disable target branch filter option on merge requests dashboard
merge_request:
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fixed merge request tabs overlapping with system header
merge_request:
author:
type: fixed

View File

@ -12,7 +12,7 @@ Rails.application.configure do
config.public_file_server.enabled = false
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
config.assets.js_compressor = :terser
# config.assets.css_compressor = :sass
# Don't fallback to assets pipeline if a precompiled asset is missed

View File

@ -1,6 +1,6 @@
name: additional_snowplow_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088
rollout_issue_url:
group: group::product_analytics
group: group::product analytics
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: ci_child_of_child_pipeline
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41102
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/243747
group: 'group::continuous integration'
group: group::continuous integration
type: development
default_enabled: true

View File

@ -2,6 +2,6 @@
name: ci_lint_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42401
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249661
group: group::continuous intergration
group: group::continuous integration
type: development
default_enabled: false

View File

@ -0,0 +1,7 @@
---
name: container_expiration_policies_historic_entry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44444
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262639
type: development
group: group::package
default_enabled: false

View File

@ -3,5 +3,5 @@ name: deploy_boards_dedupe_instances
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40768
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258214
type: development
group: group::progressive-delivery
group: group::progressive delivery
default_enabled: false

View File

@ -2,6 +2,6 @@
name: drop_license_management_artifact
introduced_by_url:
rollout_issue_url:
group: composition_analysis
group: group::composition analysis
type: development
default_enabled: true

View File

@ -2,6 +2,6 @@
name: ingress_modsecurity
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20194
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258554
group: "group::container security"
group: group::container security
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: junit_pipeline_screenshots_view
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/202114
rollout_issue_url:
group: 'group::verify testing'
group: group::verify testing
type: development
default_enabled: false

View File

@ -1,7 +0,0 @@
---
name: merge_ref_head_comments
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -1,7 +0,0 @@
---
name: modifed_path_ci_variables
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: product_analytics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443
rollout_issue_url:
group: group::product_analytics
group: group::product analytics
type: development
default_enabled: false

View File

@ -3,5 +3,5 @@ name: project_finder_similarity_sort
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43136
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263249
type: development
group: group::threat_insights
group: group::threat insights
default_enabled: false

View File

@ -0,0 +1,7 @@
---
name: push_rules_supersede_code_owners
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44126
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262019
type: development
group: group::source code
default_enabled: false

View File

@ -2,6 +2,6 @@
name: rebalance_issues
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40124
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239344
group: 'group::project management'
group: group::project management
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: save_raw_usage_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38457
rollout_issue_url:
group: group::product_analytics
group: group::product analytics
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: track_issue_activity_actions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40904
rollout_issue_url:
group: group::project_management
group: group::project management
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: usage_data_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301
rollout_issue_url:
group: group::product_analytics
group: group::product analytics
type: development
default_enabled: false

View File

@ -2,6 +2,6 @@
name: usage_data_i_source_code_code_intelligence
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41881
rollout_issue_url:
group: group::source_code
group: group::source code
type: development
default_enabled: true

View File

@ -0,0 +1 @@
Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor

View File

@ -26,7 +26,7 @@ production:
# http://redis.io/topics/sentinel
#
# You must specify a list of a few sentinels that will handle client connection
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
##
# url: redis://master:6380
# sentinels:

View File

@ -26,7 +26,7 @@ production:
# http://redis.io/topics/sentinel
#
# You must specify a list of a few sentinels that will handle client connection
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
##
# url: redis://master:6381
# sentinels:

View File

@ -26,7 +26,7 @@ production:
# http://redis.io/topics/sentinel
#
# You must specify a list of a few sentinels that will handle client connection
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
##
# url: redis://master:6382
# sentinels:

View File

@ -22,7 +22,7 @@ production:
# http://redis.io/topics/sentinel
#
# You must specify a list of a few sentinels that will handle client connection
# please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
# please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
##
# url: redis://master:6379
# sentinels:

View File

@ -368,7 +368,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :jira, only: [:show], controller: :jira
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :raw
post :mark_as_spam

View File

@ -1,4 +1,4 @@
resources :snippets, concerns: :awardable do
resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :raw
post :mark_as_spam

View File

@ -3,17 +3,31 @@
require './spec/support/sidekiq_middleware'
Gitlab::Seeder.quiet do
chance_for_decrement = 0.1 # 10% chance that we'll generate smaller count than the previous count
max_increase = 10000
max_decrease = 1000
model_class = Analytics::InstanceStatistics::Measurement
recorded_at = Date.today
# Insert random counts for the last 60 days
measurements = 60.times.flat_map do
recorded_at = (recorded_at - 1.day).end_of_day - 5.minutes
measurements = model_class.identifiers.flat_map do |_, id|
recorded_at = 60.days.ago
current_count = rand(1_000_000)
# Insert random counts for the last 60 days
Array.new(60) do
recorded_at = (recorded_at + 1.day).end_of_day - 5.minutes
# Normally our counts should slowly increase as the gitlab instance grows.
# Small chance (10%) to have a slight decrease (simulating cleanups, bulk delete)
if rand < chance_for_decrement
current_count -= rand(max_decrease)
else
current_count += rand(max_increase)
end
model_class.identifiers.map do |_, id|
{
recorded_at: recorded_at,
count: rand(1_000_000),
count: current_count,
identifier: id
}
end

View File

@ -104,12 +104,12 @@ The following table lists available parameters for jobs:
| [`script`](#script) | Shell script that is executed by a runner. |
| [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. |
| [`allow_failure`](#allow_failure) | Allow job to fail. Failed job does not contribute to commit status. |
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`. |
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, and `artifacts:reports`. |
| [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. |
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, `cache:when`, and `cache:policy`. |
| [`coverage`](#coverage) | Code coverage settings for a given job. |
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in`, and `environment:action`. |
| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). |
| [`extends`](#extends) | Configuration entries that this job inherits from. |
| [`image`](#image) | Use Docker images. Also available: `image:name` and `image:entrypoint`. |
@ -2914,6 +2914,28 @@ rspec:
- binaries/
```
#### `cache:when`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18969) in GitLab 13.5 and GitLab Runner v13.5.0.
`cache:when` defines when to save the cache, based on the status of the job. You can
set `cache:when` to:
- `on_success` - save the cache only when the job succeeds. This is the default.
- `on_failure` - save the cache only when the job fails.
- `always` - save the cache regardless of the job status.
For example, to store a cache whether or not the job fails or succeeds:
```yaml
rspec:
script: rspec
cache:
paths:
- rspec/
when: 'always'
```
#### `cache:policy`
> Introduced in GitLab 9.4.
@ -3236,7 +3258,7 @@ failure.
1. `on_failure` - upload artifacts only when the job fails.
1. `always` - upload artifacts regardless of the job status.
To upload artifacts only when job fails:
For example, to upload artifacts only when a job fails:
```yaml
job:

View File

@ -76,6 +76,10 @@ How we use SVG for our [Icons and Illustrations](icons.md).
General information about frontend [dependencies](dependencies.md) and how we manage them.
## Keyboard Shortcuts
How we implement [keyboard shortcuts](keyboard_shortcuts.md) that can be customized and disabled.
## Frontend FAQ
Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful information.

View File

@ -0,0 +1,98 @@
# Implementing keyboard shortcuts
We use [Mousetrap](https://craig.is/killing/mice) to implement keyboard
shortcuts in GitLab.
Mousetrap provides an API that allows keyboard shortcut strings (like
`mod+shift+p` or `p b`) to be bound to a JavaScript handler:
```javascript
// Don't do this; see note below
Mousetrap.bind('p b', togglePerformanceBar)
```
However, associating a hard-coded key sequence to a handler (as shown above)
prevents these keyboard shortcuts from being customized or disabled by users.
To allow keyboard shortcuts to be customized, commands are defined in
`~/behaviors/shortcuts/keybindings.js`. The `keysFor` method is responsible for
returning the correct key sequence for the provided command:
```javascript
import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings'
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), togglePerformanceBar);
```
## Shortcut customization
`keybindings.js` stores keyboard shortcut customizations as a JSON string in
`localStorage`. When `keybindings.js` is first imported, it fetches any
customizations from `localStorage` and merges these customizations into the
default set of keybindings. There is no UI to edit these customizations.
## Adding new shortcuts
Because keyboard shortcuts can be customized or disabled by end users,
developers are encouraged to build _lots_ of keyboard shortcuts into GitLab.
Shortcuts that are less likely to be used should be
[disabled](#disabling-shortcuts) by default.
To add a new shortcut, define and export a new command string in
`keybindings.js`:
```javascript
export const MAKE_COFFEE = 'foodAndBeverage.makeCoffee';
```
Next, add a new command definition under the appropriate group in the
`keybindingGroups` array:
```javascript
{
description: s__('KeyboardShortcuts|Make coffee'),
command: MAKE_COFFEE,
defaultKeys: ['mod+shift+c'],
customKeys: customizations[MAKE_COFFEE],
}
```
Finally, in the application code, import the `keysFor` function and the new
command and bind the shortcut to the handler using Mousetrap:
```javascript
import { keysFor, MAKE_COFFEE } from '~/behaviors/shortcuts/keybindings'
Mousetrap.bind(keysFor(MAKE_COFFEE), makeCoffee);
```
See the existing the command definitions in `keybindings.js` for more examples.
## Disabling shortcuts
A shortcut can be disabled, also known as _unassigned_, by assigning the
shortcut to an empty array `[]`. For example, to introduce a new shortcut that
is disabled by default, a command can be defined like this:
```javascript
export const MAKE_MOCHA = 'foodAndBeverage.makeMocha';
{
description: s__('KeyboardShortcuts|Make a mocha'),
command: MAKE_MOCHA,
defaultKeys: [],
customKeys: customizations[MAKE_MOCHA],
}
```
## Make cross-platform shortcuts
It's difficult to make shortcuts that work well in all platforms and browsers.
This is one of the reasons that being able to customize and disable shortcuts is
so important.
One important way to make keyboard shortcuts more portable is to use the `mod`
shortcut string, which resolves to `command` on Mac and `ctrl` otherwise.
See [Mousetrap's documentation](https://craig.is/killing/mice#api.bind.combo)
for more information.

View File

@ -1419,26 +1419,20 @@ Example:
```markdown
| header 1 | header 2 | header 3 |
| --- | ------ |---------:|
| --- | ------ |----------|
| cell 1 | cell 2 | cell 3 |
| cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. |
| cell 7 | | cell <br> 9 |
| cell 10 | <ul><li> - [ ] Task One </li></ul> | <ul><li> - [ ] Task Two </li><li> - [ ] Task Three </li></ul> |
| cell 7 | | cell 9 |
```
| header 1 | header 2 | header 3 |
| --- | ------ |---------:|
| --- | ------ |----------|
| cell 1 | cell 2 | cell 3 |
| cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. |
| cell 7 | | cell <br> 9 |
| cell 10 | <ul><li> - [ ] Task One </li></ul> | <ul><li> - [ ] Task Two </li><li> - [ ] Task Three </li></ul> |
| cell 7 | | cell 9 |
Additionally, you can choose the alignment of text within columns by adding colons (`:`)
to the sides of the "dash" lines in the second row. This affects every cell in the column.
NOTE: **Note:**
[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables),
the headers are always left-aligned in Chrome and Firefox, and centered in Safari.
to the sides of the "dash" lines in the second row. This affects every cell in the column:
```markdown
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
@ -1452,6 +1446,34 @@ the headers are always left-aligned in Chrome and Firefox, and centered in Safar
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables),
the headers are always left-aligned in Chrome and Firefox, and centered in Safari.
You can use HTML formatting to adjust the rendering of tables. For example, you can
use `<br>` tags to force a cell to have multiple lines:
```markdown
| Name | Details |
|------|---------|
| Item1 | This is on one line |
| Item2 | This item has:<br>- Multiple items<br>- That we want listed separately |
```
| Name | Details |
|------|---------|
| Item1 | This is on one line |
| Item2 | This item has:<br>- Multiple items<br>- That we want listed separately |
You can use HTML formatting within GitLab itself to add [task lists](#task-lists) with checkboxes,
but they do not render properly on `docs.gitlab.com`:
```markdown
| header 1 | header 2 |
|----------|----------|
| cell 1 | cell 2 |
| cell 3 | <ul><li> - [ ] Task one </li><li> - [ ] Task two </li></ul> |
```
#### Copy from spreadsheet and paste in Markdown
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27205) in GitLab 12.7.

View File

@ -469,6 +469,20 @@ Cleanup policies can be run on all projects, with these exceptions:
There are performance risks with enabling it for all projects, especially if you
are using an [external registry](./index.md#use-with-external-container-registries).
- For self-managed GitLab instances, you can enable or disable the cleanup policy for a specific
project.
To enable it:
```ruby
Feature.enable(:container_expiration_policies_historic_entry, Project.find(<project id>))
```
To disable it:
```ruby
Feature.disable(:container_expiration_policies_historic_entry, Project.find(<project id>))
```
### How the cleanup policy works

View File

@ -4,7 +4,7 @@ module API
module Entities
module JobRequest
class Cache < Grape::Entity
expose :key, :untracked, :paths, :policy
expose :key, :untracked, :paths, :policy, :when
end
end
end

View File

@ -9,14 +9,28 @@ module Gitlab
#
class Cache < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths policy].freeze
ALLOWED_KEYS = %i[key untracked paths when policy].freeze
ALLOWED_POLICY = %w[pull-push push pull].freeze
DEFAULT_POLICY = 'pull-push'
ALLOWED_WHEN = %w[on_success on_failure always].freeze
DEFAULT_WHEN = 'on_success'
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :policy, inclusion: { in: %w[pull-push push pull], message: 'should be pull-push, push, or pull' }, allow_blank: true
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
validates :policy,
inclusion: { in: ALLOWED_POLICY, message: 'should be pull-push, push, or pull' },
allow_blank: true
with_options allow_nil: true do
validates :when,
inclusion: {
in: ALLOWED_WHEN,
message: 'should be on_success, on_failure or always'
}
end
end
entry :key, Entry::Key,
@ -28,13 +42,15 @@ module Gitlab
entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
attributes :policy
attributes :policy, :when
def value
result = super
result[:key] = key_value
result[:policy] = policy || DEFAULT_POLICY
# Use self.when to avoid conflict with reserved word
result[:when] = self.when || DEFAULT_WHEN
result
end

View File

@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a set of needs dependencies.
#
class Needs < ::Gitlab::Config::Entry::Node
class Needs < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@ -29,27 +29,16 @@ module Gitlab
end
end
def compose!(deps = nil)
super(deps) do
[@config].flatten.each_with_index do |need, index|
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
.value(need)
.with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
def value
values = @entries.values.select(&:type)
values = @entries.select(&:type)
values.group_by(&:type).transform_values do |values|
values.map(&:value)
end
end
def composable_class
Entry::Need
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a configuration of the ports of a Docker service.
#
class Ports < ::Gitlab::Config::Entry::Node
class Ports < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@ -16,28 +16,8 @@ module Gitlab
validates :config, port_unique: true
end
def compose!(deps = nil)
super do
@entries = []
@config.each do |config|
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port)
.value(config || {})
.with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each do |entry|
entry.compose!(deps)
end
end
end
def value
@entries.map(&:value)
end
def descendants
@entries
def composable_class
Entry::Port
end
end
end

View File

@ -4,7 +4,7 @@ module Gitlab
module Ci
class Config
module Entry
class Rules < ::Gitlab::Config::Entry::Node
class Rules < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@ -12,24 +12,13 @@ module Gitlab
validates :config, type: Array
end
def compose!(deps = nil)
super(deps) do
@config.each_with_index do |rule, index|
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
.value(rule)
.with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
def value
@config
end
def composable_class
Entry::Rules::Rule
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
##
# Entry that represents a configuration of Docker services.
#
class Services < ::Gitlab::Config::Entry::Node
class Services < ::Gitlab::Config::Entry::ComposableArray
include ::Gitlab::Config::Entry::Validatable
validations do
@ -15,28 +15,8 @@ module Gitlab
validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) }
end
def compose!(deps = nil)
super do
@entries = []
@config.each do |config|
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {})
.with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each do |entry|
entry.compose!(deps)
end
end
end
def value
@entries.map(&:value)
end
def descendants
@entries
def composable_class
Entry::Service
end
end
end

View File

@ -13,6 +13,7 @@ module Gitlab
@paths = local_cache.delete(:paths)
@policy = local_cache.delete(:policy)
@untracked = local_cache.delete(:untracked)
@when = local_cache.delete(:when)
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
end
@ -24,7 +25,8 @@ module Gitlab
key: key_string,
paths: @paths,
policy: @policy,
untracked: @untracked
untracked: @untracked,
when: @when
}.compact.presence
}.compact
}

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
module Gitlab
module Config
module Entry
##
# Entry that represents a composable array definition
#
class ComposableArray < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include Gitlab::Utils::StrongMemoize
# TODO: Refactor `Validatable` code so that validations can apply to a child class
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
validations do
validates :config, type: Array
end
def compose!(deps = nil)
super do
@entries = Array(@entries)
# TODO: Isolate handling for a hash via: `[@config].flatten` to the `Needs` entry
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/264376
[@config].flatten.each_with_index do |value, index|
raise ArgumentError, 'Missing Composable class' unless composable_class
composable_class_name = composable_class.name.demodulize.underscore
@entries << ::Gitlab::Config::Entry::Factory.new(composable_class)
.value(value)
.with(key: composable_class_name, parent: self, description: "#{composable_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each do |entry|
entry.compose!(deps)
end
end
end
def value
@entries.map(&:value)
end
def descendants
@entries
end
def composable_class
strong_memoize(:composable_class) do
opt(:composable_class)
end
end
end
end
end
end

View File

@ -10,7 +10,7 @@ module Gitlab
class ComposableHash < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
# TODO: Refactor Validatable so these validations will not apply to a child class
# TODO: Refactor `Validatable` code so that validations can apply to a child class
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231
validations do
validates :config, type: Hash

View File

@ -37,7 +37,7 @@ module SystemCheck
@custom_error_message
)
for_more_information(
'doc/administration/high_availability/redis.md#provide-your-own-redis-instance'
'doc/administration/redis/index.html#redis-replication-and-failover-using-the-non-bundled-redis'
)
fix_and_rerun
end

View File

@ -38,7 +38,7 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
# leave only the required changes.
unless system(*%w(git checkout -- locale/*/gitlab.po))
unless system(*%w(git -c core.hooksPath=/dev/null checkout -- locale/*/gitlab.po))
raise 'failed to cleanup generated locale/*/gitlab.po files'
end

View File

@ -14773,6 +14773,12 @@ msgstr ""
msgid "KeyboardKey|Ctrl+"
msgstr ""
msgid "KeyboardShortcuts|Global Shortcuts"
msgstr ""
msgid "KeyboardShortcuts|Toggle the Performance Bar"
msgstr ""
msgid "Keys"
msgstr ""

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -30,6 +30,7 @@ module QA
view 'app/assets/javascripts/design_management/components/list/item.vue' do
element :design_file_name
element :design_image
element :design_status_icon
end
view 'app/assets/javascripts/design_management/pages/index.vue' do
@ -79,6 +80,11 @@ module QA
raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found
end
def update_design(filename)
filepath = ::File.join('qa', 'fixtures', 'designs', 'update', filename)
add_design(filepath)
end
def click_design(filename)
click_element(:design_file_name, text: filename)
end
@ -101,6 +107,14 @@ module QA
def has_design?(filename)
has_element?(:design_file_name, text: filename)
end
def has_created_icon?
has_element?(:design_status_icon, status: 'file-addition-solid')
end
def has_modified_icon?
has_element?(:design_status_icon, status: 'file-modified-solid')
end
end
end
end

View File

@ -3,18 +3,15 @@
module QA
module Resource
class Design < Base
attr_reader :id
attr_accessor :filename
attribute :issue do
Issue.fabricate_via_api!
end
attribute :filepath do
::File.absolute_path(::File.join('spec', 'fixtures', @filename))
end
attribute :id
attribute :filename
def initialize
@update = false
@filename = 'banana_sample.gif'
end
@ -26,6 +23,12 @@ module QA
issue.add_design(filepath)
end
end
private
def filepath
::File.absolute_path(::File.join('qa', 'fixtures', 'designs', @filename))
end
end
end
end

View File

@ -41,7 +41,7 @@ module QA
context 'when using attachments in comments', :object_storage do
let(:gif_file_name) { 'banana_sample.gif' }
let(:file_to_attach) do
File.absolute_path(File.join('spec', 'fixtures', gif_file_name))
File.absolute_path(File.join('qa', 'fixtures', 'designs', gif_file_name))
end
before do

View File

@ -5,7 +5,7 @@ module QA
context 'Design Management' do
let(:issue) { Resource::Issue.fabricate_via_api! }
let(:design_filename) { 'banana_sample.gif' }
let(:design) { File.absolute_path(File.join('spec', 'fixtures', design_filename)) }
let(:design) { File.absolute_path(File.join('qa', 'fixtures', 'designs', design_filename)) }
let(:annotation) { "This design is great!" }
before do

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
context 'Design Management' do
let(:design) do
Resource::Design.fabricate! do |design|
design.filename = 'tanuki.jpg'
end
end
before do
Flow::Login.sign_in
end
it 'user adds a design and modifies it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/273' do
design.issue.visit!
Page::Project::Issue::Show.perform do |issue|
expect(issue).to have_created_icon
end
Page::Project::Issue::Show.perform do |issue|
issue.update_design(design.filename)
expect(issue).to have_modified_icon
end
end
end
end
end

View File

@ -82,215 +82,6 @@ RSpec.describe Projects::SnippetsController do
end
end
describe 'POST #create' do
def create_snippet(project, snippet_params = {}, additional_params = {})
sign_in(user)
project.add_developer(user)
post :create, params: {
namespace_id: project.namespace.to_param,
project_id: project,
project_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params)
}.merge(additional_params)
Snippet.last
end
it 'creates the snippet correctly' do
snippet = create_snippet(project, visibility_level: Snippet::PRIVATE)
expect(snippet.title).to eq('Title')
expect(snippet.content).to eq('Content')
expect(snippet.description).to eq('Description')
end
context 'when the snippet is spam' do
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
end
context 'when the snippet is private' do
it 'creates the snippet' do
expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }
.to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
it 'rejects the snippet' do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
.not_to change { Snippet.count }
expect(response).to render_template(:new)
end
it 'creates a spam log' do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
.to log_spam(title: 'Title', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :new with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
create_snippet(project, visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:new)
end
context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify with reCAPTCHA enabled' do
create_snippet(project, visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
create_snippet(project,
{ visibility_level: Snippet::PUBLIC },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(project_snippet_path(project, Snippet.last))
end
end
end
end
end
describe 'PUT #update' do
let(:visibility_level) { Snippet::PUBLIC }
let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level }
def update_snippet(snippet_params = {}, additional_params = {})
sign_in(user)
project.add_developer(user)
put :update, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: snippet,
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}.merge(additional_params)
snippet.reload
end
context 'when the snippet is spam' do
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
end
context 'when the snippet is private' do
let(:visibility_level) { Snippet::PRIVATE }
it 'updates the snippet' do
expect { update_snippet(title: 'Foo') }
.to change { snippet.reload.title }.to('Foo')
end
end
context 'when the snippet is public' do
it 'rejects the snippet' do
expect { update_snippet(title: 'Foo') }
.not_to change { snippet.reload.title }
end
it 'creates a spam log' do
expect { update_snippet(title: 'Foo') }
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo')
expect(response).to render_template(:edit)
end
context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify with reCAPTCHA enabled' do
update_snippet(title: 'Foo')
expect(response).to render_template(:verify)
end
it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
snippet = update_snippet({ title: spammy_title },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(project_snippet_path(project, snippet))
end
end
end
context 'when the private snippet is made public' do
let(:visibility_level) { Snippet::PRIVATE }
it 'rejects the snippet' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
.not_to change { snippet.reload.title }
end
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:edit)
end
context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify' do
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
it 'renders snippet page' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(project_snippet_path(project, snippet))
end
end
end
end
end
describe 'POST #mark_as_spam' do
let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) }
@ -329,12 +120,6 @@ RSpec.describe Projects::SnippetsController do
expect(assigns(:snippet)).to eq(project_snippet)
expect(response).to have_gitlab_http_status(:ok)
end
it 'renders the blob from the repository' do
subject
expect(assigns(:blob)).to eq(project_snippet.blobs.first)
end
end
%w[show raw].each do |action|
@ -395,6 +180,16 @@ RSpec.describe Projects::SnippetsController do
end
end
describe 'GET #show as JSON' do
it 'renders the blob from the repository' do
project_snippet = create(:project_snippet, :public, :repository, project: project, author: user)
get :show, params: { namespace_id: project.namespace, project_id: project, id: project_snippet.to_param }, format: :json
expect(assigns(:blob)).to eq(project_snippet.blobs.first)
end
end
describe "GET #show for embeddable content" do
let(:project_snippet) { create(:project_snippet, :repository, snippet_permission, project: project, author: user) }
let(:extra_params) { {} }
@ -533,62 +328,4 @@ RSpec.describe Projects::SnippetsController do
it_behaves_like 'content disposition headers'
end
end
describe 'DELETE #destroy' do
let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) }
let(:params) do
{
namespace_id: project.namespace.to_param,
project_id: project,
id: snippet.to_param
}
end
subject { delete :destroy, params: params }
context 'when current user has ability to destroy the snippet' do
before do
sign_in(user)
end
it 'removes the snippet' do
subject
expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'when snippet is succesfuly destroyed' do
it 'redirects to the project snippets page' do
subject
expect(response).to redirect_to(project_snippets_path(project))
end
end
context 'when snippet is not destroyed' do
before do
allow(snippet).to receive(:destroy).and_return(false)
controller.instance_variable_set(:@snippet, snippet)
end
it 'renders the snippet page with errors' do
subject
expect(flash[:alert]).to eq('Failed to remove snippet.')
expect(response).to redirect_to(project_snippet_path(project, snippet))
end
end
end
context 'when current_user does not have ability to destroy the snippet' do
it 'responds with status 404' do
sign_in(other_user)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View File

@ -86,12 +86,6 @@ RSpec.describe SnippetsController do
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_gitlab_http_status(:ok)
end
it 'renders the blob from the repository' do
subject
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
end
end
context 'when the personal snippet is private' do
@ -200,7 +194,7 @@ RSpec.describe SnippetsController do
end
it 'responds with status 404' do
get :show, params: { id: 'doesntexist' }
get :show, params: { id: non_existing_record_id }
expect(response).to have_gitlab_http_status(:not_found)
end
@ -208,234 +202,20 @@ RSpec.describe SnippetsController do
context 'when not signed in' do
it 'responds with status 404' do
get :show, params: { id: 'doesntexist' }
get :show, params: { id: non_existing_record_id }
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
describe 'POST #create' do
def create_snippet(snippet_params = {}, additional_params = {})
sign_in(user)
context 'when requesting JSON' do
it 'renders the blob from the repository' do
personal_snippet = create(:personal_snippet, :public, :repository, author: user)
post :create, params: {
personal_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params)
}.merge(additional_params)
get :show, params: { id: personal_snippet.to_param }, format: :json
Snippet.last
end
it 'creates the snippet correctly' do
snippet = create_snippet(visibility_level: Snippet::PRIVATE)
expect(snippet.title).to eq('Title')
expect(snippet.content).to eq('Content')
expect(snippet.description).to eq('Description')
end
context 'when user is not allowed to create a personal snippet' do
let(:user) { create(:user, :external) }
it 'responds with status 404' do
aggregate_failures do
expect do
create_snippet(visibility_level: Snippet::PUBLIC)
end.not_to change { Snippet.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when the controller receives the files param' do
let(:files) { %w(foo bar) }
it 'passes the files param to the snippet create service' do
expect(Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: files)).and_call_original
create_snippet({ title: nil }, { files: files })
end
end
context 'when the snippet is spam' do
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
end
context 'when the snippet is private' do
it 'creates the snippet' do
expect { create_snippet(visibility_level: Snippet::PRIVATE) }
.to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
it 'rejects the snippet' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
.not_to change { Snippet.count }
end
it 'creates a spam log' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
.to log_spam(title: 'Title', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :new with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
create_snippet(visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:new)
end
context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify' do
create_snippet(visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
it 'renders snippet page' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
snippet = create_snippet({ title: spammy_title },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(snippet_path(snippet))
end
end
end
end
end
describe 'PUT #update' do
let(:project) { create :project }
let(:visibility_level) { Snippet::PUBLIC }
let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level }
def update_snippet(snippet_params = {}, additional_params = {})
sign_in(user)
put :update, params: {
id: snippet.id,
personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}.merge(additional_params)
snippet.reload
end
context 'when the snippet is spam' do
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
end
context 'when the snippet is private' do
let(:visibility_level) { Snippet::PRIVATE }
it 'updates the snippet' do
expect { update_snippet(title: 'Foo') }
.to change { snippet.reload.title }.to('Foo')
end
end
context 'when a private snippet is made public' do
let(:visibility_level) { Snippet::PRIVATE }
it 'rejects the snippet' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
.not_to change { snippet.reload.title }
end
it 'creates a spam log' do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:edit)
end
context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify' do
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(snippet_path(snippet))
end
end
end
context 'when the snippet is public' do
it 'rejects the snippet' do
expect { update_snippet(title: 'Foo') }
.not_to change { snippet.reload.title }
end
it 'creates a spam log' do
expect {update_snippet(title: 'Foo') }
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo')
expect(response).to render_template(:edit)
end
context 'recaptcha enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'renders :verify' do
update_snippet(title: 'Foo')
expect(response).to render_template(:verify)
end
it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
snippet = update_snippet({ title: spammy_title },
{ spam_log_id: spam_logs.last.id,
recaptcha_verification: true })
expect(response).to redirect_to(snippet_path(snippet))
end
end
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
end
end
end
@ -632,7 +412,7 @@ RSpec.describe SnippetsController do
end
it 'responds with status 404' do
get :raw, params: { id: 'doesntexist' }
get :raw, params: { id: non_existing_record_id }
expect(response).to have_gitlab_http_status(:not_found)
end
@ -640,7 +420,7 @@ RSpec.describe SnippetsController do
context 'when not signed in' do
it 'redirects to the sign in path' do
get :raw, params: { id: 'doesntexist' }
get :raw, params: { id: non_existing_record_id }
expect(response).to redirect_to(new_user_session_path)
end
@ -688,56 +468,4 @@ RSpec.describe SnippetsController do
expect(json_response.keys).to match_array(%w(body references))
end
end
describe 'DELETE #destroy' do
let!(:snippet) { create :personal_snippet, author: user }
context 'when current user has ability to destroy the snippet' do
before do
sign_in(user)
end
it 'removes the snippet' do
delete :destroy, params: { id: snippet.to_param }
expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'when snippet is succesfuly destroyed' do
it 'redirects to the project snippets page' do
delete :destroy, params: { id: snippet.to_param }
expect(response).to redirect_to(dashboard_snippets_path)
end
end
context 'when snippet is not destroyed' do
before do
allow(snippet).to receive(:destroy).and_return(false)
controller.instance_variable_set(:@snippet, snippet)
end
it 'renders the snippet page with errors' do
delete :destroy, params: { id: snippet.to_param }
expect(flash[:alert]).to eq('Failed to remove snippet.')
expect(response).to redirect_to(snippet_path(snippet))
end
end
end
context 'when current_user does not have ability to destroy the snippet' do
let(:another_user) { create(:user) }
before do
sign_in(another_user)
end
it 'responds with status 404' do
delete :destroy, params: { id: snippet.to_param }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View File

@ -384,7 +384,8 @@ FactoryBot.define do
key: 'cache_key',
untracked: false,
paths: ['vendor/*'],
policy: 'pull-push'
policy: 'pull-push',
when: 'on_success'
}
}
end

View File

@ -19,6 +19,12 @@ RSpec.describe 'Dashboard Merge Requests' do
sign_in(current_user)
end
it 'disables target branch filter' do
visit merge_requests_dashboard_path
expect(page).not_to have_selector('#js-dropdown-target-branch', visible: false)
end
context 'new merge request dropdown' do
let(:project_with_disabled_merge_requests) { create(:project, :merge_requests_disabled) }

View File

@ -3,27 +3,35 @@
require 'spec_helper'
RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, container_registry_enabled: container_registry_enabled) }
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
let(:container_registry_enabled) { true }
let(:container_registry_enabled_on_project) { true }
subject { visit project_settings_ci_cd_path(project) }
before do
project.update!(container_registry_enabled: container_registry_enabled_on_project)
sign_in(user)
stub_container_registry_config(enabled: true)
stub_container_registry_config(enabled: container_registry_enabled)
stub_feature_flags(new_variables_ui: false)
end
context 'as owner' do
before do
visit project_settings_ci_cd_path(project)
end
it 'shows available section' do
subject
settings_block = find('#js-registry-policies')
expect(settings_block).to have_text 'Cleanup policy for tags'
end
it 'saves cleanup policy submit the form' do
subject
within '#js-registry-policies' do
within '.card-body' do
select('7 days until tags are automatically removed', from: 'Expiration interval:')
@ -40,6 +48,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
end
it 'does not save cleanup policy submit form with invalid regex' do
subject
within '#js-registry-policies' do
within '.card-body' do
fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
@ -53,25 +63,53 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
end
end
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
visit project_settings_ci_cd_path(project)
context 'with a project without expiration policy' do
where(:application_setting, :feature_flag, :result) do
true | true | :available_section
true | false | :available_section
false | true | :available_section
false | false | :disabled_message
end
with_them do
before do
project.container_expiration_policy.destroy!
stub_feature_flags(container_expiration_policies_historic_entry: false)
stub_application_setting(container_expiration_policies_enable_historic_entries: application_setting)
stub_feature_flags(container_expiration_policies_historic_entry: project) if feature_flag
end
it 'displays the expected result' do
subject
within '#js-registry-policies' do
case result
when :available_section
expect(find('.card-header')).to have_content('Tag expiration policy')
when :disabled_message
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
end
end
end
end
end
context 'when registry is disabled' do
let(:container_registry_enabled) { false }
it 'does not exists' do
subject
expect(page).not_to have_selector('#js-registry-policies')
end
end
context 'when container registry is disabled on project' do
let(:container_registry_enabled) { false }
before do
visit project_settings_ci_cd_path(project)
end
let(:container_registry_enabled_on_project) { false }
it 'does not exists' do
subject
expect(page).not_to have_selector('#js-registry-policies')
end
end

View File

@ -0,0 +1,66 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
describe('~/behaviors/shortcuts/keybindings.js', () => {
let keysFor;
let TOGGLE_PERFORMANCE_BAR;
let LOCAL_STORAGE_KEY;
beforeAll(() => {
useLocalStorageSpy();
});
const setupCustomizations = async customizationsAsString => {
localStorage.clear();
if (customizationsAsString) {
localStorage.setItem(LOCAL_STORAGE_KEY, customizationsAsString);
}
jest.resetModules();
({ keysFor, TOGGLE_PERFORMANCE_BAR, LOCAL_STORAGE_KEY } = await import(
'~/behaviors/shortcuts/keybindings'
));
};
describe('when a command has not been customized', () => {
beforeEach(async () => {
await setupCustomizations('{}');
});
it('returns the default keybinding for the command', () => {
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
});
});
describe('when a command has been customized', () => {
const customization = ['p b a r'];
beforeEach(async () => {
await setupCustomizations(JSON.stringify({ [TOGGLE_PERFORMANCE_BAR]: customization }));
});
it('returns the default keybinding for the command', () => {
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(customization);
});
});
describe("when the localStorage entry isn't valid JSON", () => {
beforeEach(async () => {
await setupCustomizations('{');
});
it('returns the default keybinding for the command', () => {
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
});
});
describe(`when localStorage doesn't contain the ${LOCAL_STORAGE_KEY} key`, () => {
beforeEach(async () => {
await setupCustomizations();
});
it('returns the default keybinding for the command', () => {
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
});
});
});

View File

@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { createStore } from '~/mr_notes/stores';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
@ -28,13 +27,6 @@ describe('InlineDiffTableRow', () => {
});
};
const setWindowLocation = value => {
Object.defineProperty(window, 'location', {
writable: true,
value,
});
};
beforeEach(() => {
store = createStore();
store.state.notes.userData = TEST_USER;
@ -122,22 +114,15 @@ describe('InlineDiffTableRow', () => {
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
it.each`
userData | query | mergeRefHeadComments | expectation
${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
${null} | ${''} | ${true} | ${false}
`(
'exists is $expectation - with userData ($userData) query ($query)',
({ userData, query, mergeRefHeadComments, expectation }) => {
store.state.notes.userData = userData;
gon.features = { mergeRefHeadComments };
setWindowLocation({ href: `${TEST_HOST}?${query}` });
createComponent({}, store);
userData | expectation
${TEST_USER} | ${true}
${null} | ${false}
`('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => {
store.state.notes.userData = userData;
createComponent({}, store);
expect(findNoteButton().exists()).toBe(expectation);
},
);
expect(findNoteButton().exists()).toBe(expectation);
});
it.each`
isHover | line | expectation

View File

@ -1,7 +1,6 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { createStore } from '~/mr_notes/stores';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import diffFileMockData from '../mock_data/diff_file';
@ -186,13 +185,6 @@ describe('ParallelDiffTableRow', () => {
});
};
const setWindowLocation = value => {
Object.defineProperty(window, 'location', {
writable: true,
value,
});
};
beforeEach(() => {
// eslint-disable-next-line prefer-destructuring
thisLine = diffFileMockData.parallel_diff_lines[2];
@ -228,19 +220,15 @@ describe('ParallelDiffTableRow', () => {
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' });
it.each`
hover | line | userData | query | mergeRefHeadComments | expectation
${true} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
${true} | ${{}} | ${null} | ${''} | ${true} | ${false}
${false} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
hover | line | userData | expectation
${true} | ${{}} | ${TEST_USER} | ${true}
${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false}
${true} | ${{}} | ${null} | ${false}
${false} | ${{}} | ${TEST_USER} | ${false}
`(
'exists is $expectation - with userData ($userData) query ($query)',
async ({ hover, line, userData, query, mergeRefHeadComments, expectation }) => {
'exists is $expectation - with userData ($userData)',
async ({ hover, line, userData, expectation }) => {
store.state.notes.userData = userData;
gon.features = { mergeRefHeadComments };
setWindowLocation({ href: `${TEST_HOST}?${query}` });
createComponent(line, store);
if (hover) await wrapper.find('.line_holder').trigger('mouseover');

View File

@ -0,0 +1,132 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
const issuableHeaderProps = {
...mockIssuable,
...mockIssuableShowProps,
};
const createComponent = (propsData = issuableHeaderProps) =>
shallowMount(IssuableHeader, {
propsData,
slots: {
'status-badge': 'Open',
'header-actions': `
<button class="js-close">Close issuable</button>
<a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a>
`,
},
});
describe('IssuableHeader', () => {
let wrapper;
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('authorId', () => {
it('returns numeric ID from GraphQL ID of `author` prop', () => {
expect(wrapper.vm.authorId).toBe(1);
});
});
});
describe('handleRightSidebarToggleClick', () => {
beforeEach(() => {
setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
});
it('dispatches `click` event on sidebar toggle button', () => {
wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
wrapper.vm.handleRightSidebarToggleClick();
expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: 'click',
}),
);
});
});
describe('template', () => {
it('renders issuable status icon and text', () => {
const statusBoxEl = findByTestId('status');
expect(statusBoxEl.exists()).toBe(true);
expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
expect(statusBoxEl.text()).toContain('Open');
});
it('renders blocked icon when issuable is blocked', async () => {
wrapper.setProps({
blocked: true,
});
await wrapper.vm.$nextTick();
const blockedEl = findByTestId('blocked');
expect(blockedEl.exists()).toBe(true);
expect(blockedEl.find(GlIcon).props('name')).toBe('lock');
});
it('renders confidential icon when issuable is confidential', async () => {
wrapper.setProps({
confidential: true,
});
await wrapper.vm.$nextTick();
const confidentialEl = findByTestId('confidential');
expect(confidentialEl.exists()).toBe(true);
expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash');
});
it('renders issuable author avatar', () => {
const { username, name, webUrl, avatarUrl } = mockIssuable.author;
const avatarElAttrs = {
'data-user-id': '1',
'data-username': username,
'data-name': name,
href: webUrl,
target: '_blank',
};
const avatarEl = findByTestId('avatar');
expect(avatarEl.exists()).toBe(true);
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({
size: '24',
src: avatarUrl,
label: name,
});
});
it('renders sidebar toggle button', () => {
const toggleButtonEl = findByTestId('sidebar-toggle');
expect(toggleButtonEl.exists()).toBe(true);
expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left');
});
it('renders header actions', () => {
const actionsEl = findByTestId('header-actions');
expect(actionsEl.find('button.js-close').exists()).toBe(true);
expect(actionsEl.find('a.js-new').exists()).toBe(true);
});
});
});

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