Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-16 18:09:47 +00:00
parent 6de7d2c195
commit bf1600d157
123 changed files with 3794 additions and 1122 deletions

View File

@ -49,6 +49,7 @@
"Elasticsearch",
"Facebook",
"fastlane",
"fluent-plugin-redis-slowlog",
"GDK",
"Geo",
"Git LFS",

View File

@ -1 +1 @@
ab2f2386ab69575cd0a58f7279be707a17d7a6c8
b670554eae8643f2072d3b4f6f7c5cd2b9ec8776

View File

@ -123,7 +123,7 @@ export default {
</div>
<template v-if="!isEditing">
<div
class="note-text js-note-text"
class="note-text js-note-text md"
data-qa-selector="note_content"
v-html="note.bodyHtml"
></div>

View File

@ -1,47 +0,0 @@
<script>
import TreeActionLink from './tree_action_link.vue';
import { __ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility';
export default {
components: {
TreeActionLink,
},
props: {
projectPath: {
type: String,
required: true,
},
refSha: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: false,
default: true,
},
forkPath: {
type: String,
required: false,
default: '',
},
},
computed: {
showLinkToFork() {
return !this.canPushCode && this.forkPath;
},
text() {
return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE');
},
path() {
const path = this.showLinkToFork ? this.forkPath : this.projectPath;
return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`);
},
},
};
</script>
<template>
<tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" />
</template>

View File

@ -1,30 +1,22 @@
import Vue from 'vue';
import { escapeFileUrl } from '../lib/utils/url_utility';
import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
import WebIdeLink from './components/web_ide_link.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { parseBoolean } from '../lib/utils/common_utils';
import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
const {
canPushCode,
projectPath,
projectShortPath,
forkPath,
ref,
escapedRef,
fullName,
} = dataset;
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
apolloProvider.clients.defaultClient.cache.writeData({
@ -121,6 +113,10 @@ export default function setupVueRepositoryList() {
const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
if (webIdeLinkEl) {
const { ideBasePath, ...options } = convertObjectPropsToCamelCase(
JSON.parse(webIdeLinkEl.dataset.options),
);
// eslint-disable-next-line no-new
new Vue({
el: webIdeLinkEl,
@ -128,10 +124,10 @@ export default function setupVueRepositoryList() {
render(h) {
return h(WebIdeLink, {
props: {
projectPath,
refSha: ref,
forkPath,
canPushCode: parseBoolean(canPushCode),
webIdeUrl: webIDEUrl(
joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'),
),
...options,
},
});
},

View File

@ -0,0 +1,90 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
actions: {
type: Array,
required: true,
},
selectedKey: {
type: String,
required: false,
default: '',
},
},
computed: {
hasMultipleActions() {
return this.actions.length > 1;
},
selectedAction() {
return this.actions.find(x => x.key === this.selectedKey) || this.actions[0];
},
},
methods: {
handleItemClick(action) {
this.$emit('select', action.key);
},
handleClick(action, evt) {
return action.handle?.(evt);
},
},
};
</script>
<template>
<gl-dropdown
v-if="hasMultipleActions"
v-gl-tooltip="selectedAction.tooltip"
class="gl-button-deprecated-adapter"
:text="selectedAction.text"
:split-href="selectedAction.href"
split
@click="handleClick(selectedAction, $event)"
>
<template slot="button-content">
<span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
{{ selectedAction.text }}
</span>
</template>
<template v-for="(action, index) in actions">
<gl-dropdown-item
:key="action.key"
class="gl-dropdown-item-deprecated-adapter"
:is-check-item="true"
:is-checked="action.key === selectedAction.key"
:secondary-text="action.secondaryText"
:data-testid="`action_${action.key}`"
@click="handleItemClick(action)"
>
{{ action.text }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
</template>
</gl-dropdown>
<gl-link
v-else-if="selectedAction"
v-gl-tooltip="selectedAction.tooltip"
v-bind="selectedAction.attrs"
class="btn"
:href="selectedAction.href"
@click="handleClick(selectedAction, $event)"
>
{{ selectedAction.text }}
</gl-link>
</template>

View File

@ -0,0 +1,118 @@
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const KEY_WEB_IDE = 'webide';
const KEY_GITPOD = 'gitpod';
export default {
components: {
ActionsButton,
LocalStorageSync,
},
props: {
webIdeUrl: {
type: String,
required: true,
},
needsToFork: {
type: Boolean,
required: false,
default: false,
},
showWebIdeButton: {
type: Boolean,
required: false,
default: true,
},
showGitpodButton: {
type: Boolean,
required: false,
default: false,
},
gitpodUrl: {
type: String,
required: false,
default: '',
},
gitpodEnabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
selection: KEY_WEB_IDE,
};
},
computed: {
actions() {
return [this.webIdeAction, this.gitpodAction].filter(x => x);
},
webIdeAction() {
if (!this.showWebIdeButton) {
return null;
}
const handleOptions = this.needsToFork
? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') }
: { href: this.webIdeUrl };
return {
key: KEY_WEB_IDE,
text: __('Web IDE'),
secondaryText: __('Quickly and easily edit multiple files in your project.'),
tooltip: '',
attrs: {
'data-qa-selector': 'web_ide_button',
},
...handleOptions,
};
},
gitpodAction() {
if (!this.showGitpodButton) {
return null;
}
const handleOptions = this.gitpodEnabled
? { href: this.gitpodUrl }
: { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') };
const secondaryText = __('Launch a ready-to-code development environment for your project.');
return {
key: KEY_GITPOD,
text: __('Gitpod'),
secondaryText,
tooltip: secondaryText,
attrs: {
'data-qa-selector': 'gitpod_button',
},
...handleOptions,
};
},
},
methods: {
select(key) {
this.selection = key;
},
showModal(id) {
$(id).modal('show');
},
},
};
</script>
<template>
<div>
<actions-button :actions="actions" :selected-key="selection" @select="select" />
<local-storage-sync
storage-key="gl-web-ide-button-selected"
:value="selection"
@input="select"
/>
</div>
</template>

View File

@ -542,3 +542,13 @@ fieldset[disabled] .btn,
.btn-no-padding {
padding: 0;
}
// This class helps convert `.gl-button` children so that they consistently
// match the style of `.btn` elements which might be around them. Ideally we
// wouldn't need this class.
//
// Remove by upgrading all buttons in a container to use the new `.gl-button` style.
.gl-button-deprecated-adapter .gl-button {
box-shadow: none;
border-width: 1px;
}

View File

@ -1135,3 +1135,17 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
width: $gl-dropdown-width-wide;
}
}
.gl-dropdown-item-deprecated-adapter {
.dropdown-item {
align-items: flex-start;
.gl-new-dropdown-item-text-primary {
@include gl-font-weight-bold;
}
.gl-new-dropdown-item-text-secondary {
color: inherit;
}
}
}

View File

@ -51,6 +51,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:view_diffs_file_by_file,
:tab_width,
:sourcegraph_enabled,
:gitpod_enabled,
:render_whitespace_in_code
]
end

View File

@ -104,6 +104,7 @@ class ProfilesController < Profiles::ApplicationController
:bio,
:email,
:role,
:gitpod_enabled,
:hide_no_password,
:hide_no_ssh_key,
:hide_project_limit,

View File

@ -222,6 +222,8 @@ module ApplicationSettingsHelper
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
:gitpod_enabled,
:gitpod_url,
:grafana_enabled,
:grafana_url,
:gravatar_enabled,

View File

@ -80,6 +80,13 @@ module PreferencesHelper
)
end
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
end
end
private
# Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too

View File

@ -191,16 +191,46 @@ module TreeHelper
def vue_file_list_data(project, ref)
{
can_push_code: current_user&.can?(:push_code, project) && "true",
project_path: project.full_path,
project_short_path: project.path,
fork_path: current_user&.fork_of(project)&.full_path,
ref: ref,
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref),
full_name: project.name_with_namespace
}
end
def ide_base_path(project)
can_push_code = current_user&.can?(:push_code, project)
fork_path = current_user&.fork_of(project)&.full_path
if can_push_code
project.full_path
else
fork_path || project.full_path
end
end
def vue_ide_link_data(project, ref)
can_collaborate = can_collaborate_with_project?(project)
can_create_mr_from_fork = can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
show_web_ide_button = (can_collaborate || current_user&.already_forked?(project) || can_create_mr_from_fork)
{
ide_base_path: ide_base_path(project),
needs_to_fork: !can_collaborate && !current_user&.already_forked?(project),
show_web_ide_button: show_web_ide_button,
show_gitpod_button: show_web_ide_button && Gitlab::Gitpod.feature_and_settings_enabled?(project),
gitpod_url: full_gitpod_url(project, ref),
gitpod_enabled: current_user&.gitpod_enabled
}
end
def full_gitpod_url(project, ref)
return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(project)
"#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(project, tree_join(ref, @path || ''))}"
end
def directory_download_links(project, ref, archive_prefix)
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
{

View File

@ -132,6 +132,11 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :sourcegraph_enabled
validates :gitpod_url,
presence: true,
addressable_url: { enforce_sanitization: true },
if: :gitpod_enabled
validates :snowplow_collector_hostname,
presence: true,
hostname: true,

View File

@ -74,6 +74,8 @@ module ApplicationSettingImplementation
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitpod_enabled: false,
gitpod_url: 'https://gitpod.io/',
gravatar_enabled: Settings.gravatar['enabled'],
group_download_export_limit: 1,
group_export_limit: 6,

View File

@ -11,6 +11,8 @@ module Ci
default_value_for :data_store, :redis
after_create { metrics.increment_trace_operation(operation: :chunked) }
CHUNK_SIZE = 128.kilobytes
WRITE_LOCK_RETRY = 10
WRITE_LOCK_SLEEP = 0.01.seconds
@ -182,6 +184,8 @@ module Ci
end
current_store.append_data(self, value, offset).then do |stored|
metrics.increment_trace_operation(operation: :appended)
raise ArgumentError, 'Trace appended incorrectly' if stored != new_size
end
@ -205,5 +209,9 @@ module Ci
retries: WRITE_LOCK_RETRY,
sleep_sec: WRITE_LOCK_SLEEP }]
end
def metrics
@metrics ||= ::Gitlab::Ci::Trace::Metrics.new
end
end
end

View File

@ -27,18 +27,7 @@
#
module RelativePositioning
extend ActiveSupport::Concern
STEPS = 10
IDEAL_DISTANCE = 2**(STEPS - 1) + 1
MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
START_POSITION = 0
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
MAX_GAP = IDEAL_DISTANCE * 2
MIN_GAP = 2
NoSpaceLeft = Class.new(StandardError)
include ::Gitlab::RelativePositioning
class_methods do
def move_nulls_to_end(objects)
@ -49,56 +38,10 @@ module RelativePositioning
move_nulls(objects, at_end: false)
end
# This method takes two integer values (positions) and
# calculates the position between them. The range is huge as
# the maximum integer value is 2147483647.
#
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
#
# Then we handle one of three cases:
# - If the gap is too small, we raise NoSpaceLeft
# - If the gap is larger than MAX_GAP, we place the new position at most
# IDEAL_DISTANCE from the edge of the gap.
# - otherwise we place the new position at the midpoint.
#
# The new position will always satisfy: pos_before <= midpoint <= pos_after
#
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
# If the gap is too small, NoSpaceLeft is raised.
#
# This class method should only be called by instance methods of this module, which
# include handling for minimum gap size.
#
# @raises NoSpaceLeft
# @api private
def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort
gap_width = pos_after - pos_before
midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min
if gap_width < MIN_GAP
raise NoSpaceLeft
elsif gap_width > MAX_GAP
if pos_before <= MIN_POSITION
pos_after - IDEAL_DISTANCE
elsif pos_after >= MAX_POSITION
pos_before + IDEAL_DISTANCE
else
midpoint
end
else
midpoint
end
end
private
# @api private
def gap_size(object, gaps:, at_end:, starting_from:)
def gap_size(context, gaps:, at_end:, starting_from:)
total_width = IDEAL_DISTANCE * gaps
size = if at_end && starting_from + total_width >= MAX_POSITION
(MAX_POSITION - starting_from) / gaps
@ -108,23 +51,17 @@ module RelativePositioning
IDEAL_DISTANCE
end
# Shift max elements leftwards if there isn't enough space
return [size, starting_from] if size >= MIN_GAP
order = at_end ? :desc : :asc
terminus = object
.send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
.where('relative_position IS NOT NULL')
.order(relative_position: order)
.first
if at_end
terminus.move_sequence_before(true)
max_relative_position = terminus.reset.relative_position
terminus = context.max_sibling
terminus.shift_left
max_relative_position = terminus.relative_position
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
else
terminus.move_sequence_after(true)
min_relative_position = terminus.reset.relative_position
terminus = context.min_sibling
terminus.shift_right
min_relative_position = terminus.relative_position
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
end
end
@ -142,8 +79,9 @@ module RelativePositioning
objects = objects.reject(&:relative_position)
return 0 if objects.empty?
representative = objects.first
number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
representative = RelativePositioning.mover.context(objects.first)
position = if at_end
representative.max_relative_position
else
@ -198,306 +136,68 @@ module RelativePositioning
end
end
def min_relative_position(&block)
calculate_relative_position('MIN', &block)
end
def max_relative_position(&block)
calculate_relative_position('MAX', &block)
end
def prev_relative_position(ignoring: nil)
prev_pos = nil
if self.relative_position
prev_pos = max_relative_position do |relation|
relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position < ?', self.relative_position)
end
end
prev_pos
end
def next_relative_position(ignoring: nil)
next_pos = nil
if self.relative_position
next_pos = min_relative_position do |relation|
relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position > ?', self.relative_position)
end
end
next_pos
def self.mover
::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
end
def move_between(before, after)
return move_after(before) unless after
return move_before(after) unless before
before, after = [before, after].sort_by(&:relative_position) if before && after
before, after = after, before if after.relative_position < before.relative_position
pos_left = before.relative_position
pos_right = after.relative_position
if pos_right - pos_left < MIN_GAP
# Not enough room! Make space by shifting all previous elements to the left
# if there is enough space, else to the right
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
if gap.present?
after.move_sequence_before(next_gap: gap)
pos_left -= optimum_delta_for_gap(gap)
else
before.move_sequence_after
pos_right = after.reset.relative_position
end
end
new_position = self.class.position_between(pos_left, pos_right)
self.relative_position = new_position
RelativePositioning.mover.move(self, before, after)
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_after(before = self)
pos_before = before.relative_position
pos_after = before.next_relative_position(ignoring: self)
if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
if gap.nil?
before.move_sequence_before(true)
pos_before = before.reset.relative_position
else
before.move_sequence_after(next_gap: gap)
pos_after += optimum_delta_for_gap(gap)
end
end
self.relative_position = self.class.position_between(pos_before, pos_after)
RelativePositioning.mover.move(self, before, nil)
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_before(after = self)
pos_after = after.relative_position
pos_before = after.prev_relative_position(ignoring: self)
if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after)
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
if gap.nil?
after.move_sequence_after(true)
pos_after = after.reset.relative_position
else
after.move_sequence_before(next_gap: gap)
pos_before -= optimum_delta_for_gap(gap)
end
end
self.relative_position = self.class.position_between(pos_before, pos_after)
RelativePositioning.mover.move(self, nil, after)
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_to_end
max_pos = max_relative_position
if max_pos.nil?
self.relative_position = START_POSITION
elsif gap_too_small?(max_pos, MAX_POSITION + 1)
max = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC')).first
max.move_sequence_before(true)
max.reset
self.relative_position = self.class.position_between(max.relative_position, MAX_POSITION + 1)
else
self.relative_position = self.class.position_between(max_pos, MAX_POSITION + 1)
end
RelativePositioning.mover.move_to_end(self)
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MAX_POSITION
rescue ActiveRecord::QueryCanceled => e
could_not_move(e)
raise e
end
def move_to_start
min_pos = min_relative_position
if min_pos.nil?
self.relative_position = START_POSITION
elsif gap_too_small?(min_pos, MIN_POSITION - 1)
min = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC')).first
min.move_sequence_after(true)
min.reset
self.relative_position = self.class.position_between(MIN_POSITION - 1, min.relative_position)
else
self.relative_position = self.class.position_between(MIN_POSITION - 1, min_pos)
end
RelativePositioning.mover.move_to_start(self)
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MIN_POSITION
rescue ActiveRecord::QueryCanceled => e
could_not_move(e)
raise e
end
# Moves the sequence before the current item to the middle of the next gap
# For example, we have
#
# 5 . . . . . 11 12 13 14 [15] 16 . 17
# -----------
#
# This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
#
# 5 . . 8 9 10 11 . . . [15] 16 . 17
# ---------
#
# Creating a gap to the left of the current item. We can understand this as
# dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the right of the current item:
#
# 5 . . 8 9 10 11 [14] . . . 16 . 17
# --------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
raise NoSpaceLeft unless next_gap.present?
delta = optimum_delta_for_gap(next_gap)
move_sequence(next_gap[:start], relative_position, -delta, include_self)
end
# Moves the sequence after the current item to the middle of the next gap
# For example, we have:
#
# 8 . 10 [11] 12 13 14 15 . . . . . 21
# -----------
#
# This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
#
# 8 . 10 [11] . . . 15 16 17 18 . . 21
# -----------
#
# Creating a gap to the right of the current item. We can understand this as
# dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the left of the current item:
#
# 8 . 10 . . . [14] 15 16 17 18 . . 21
# ----------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
raise NoSpaceLeft unless next_gap.present?
delta = optimum_delta_for_gap(next_gap)
move_sequence(relative_position, next_gap[:start], delta, include_self)
end
private
def gap_too_small?(pos_a, pos_b)
return false unless pos_a && pos_b
(pos_a - pos_b).abs < MIN_GAP
end
# Find the first suitable gap to the left of the current position.
#
# Satisfies the relations:
# - gap[:start] <= relative_position
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
#
# Supposing that the current item is 13, and we have a sequence of items:
#
# 1 . . . 5 . . . . 11 12 [13] 14 . . 17
# ^---------^
#
# Then we return: `{ start: 11, end: 5 }`
#
# Here start refers to the end of the gap closest to the current item.
def find_next_gap_before
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
.where('relative_position <= ?', relative_position)
.order(relative_position: :desc)
find_next_gap(items_with_next_pos, MIN_POSITION)
end
# Find the first suitable gap to the right of the current position.
#
# Satisfies the relations:
# - gap[:start] >= relative_position
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
#
# Supposing the current item is 13, and that we have a sequence of items:
#
# 9 . . . [13] 14 15 . . . . 20 . . . 24
# ^---------^
#
# Then we return: `{ start: 15, end: 20 }`
#
# Here start refers to the end of the gap closest to the current item.
def find_next_gap_after
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
.where('relative_position >= ?', relative_position)
.order(:relative_position)
find_next_gap(items_with_next_pos, MAX_POSITION)
end
def find_next_gap(items_with_next_pos, end_is_nil)
gap = self.class
.from(items_with_next_pos, :items)
.where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
.limit(1)
.pluck(:pos, :next_pos)
.first
return if gap.nil? || gap.first == end_is_nil
{ start: gap.first, end: gap.second || end_is_nil }
end
def optimum_delta_for_gap(gap)
delta = ((gap[:start] - gap[:end]) / 2.0).abs.ceil
[delta, IDEAL_DISTANCE].min
end
def move_sequence(start_pos, end_pos, delta, include_self = false)
relation = include_self ? scoped_items : relative_siblings
# This method is used during rebalancing - override it to customise the update
# logic:
def update_relative_siblings(relation, range, delta)
relation
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
.where(relative_position: range)
.update_all("relative_position = relative_position + #{delta}")
end
def calculate_relative_position(calculation)
# When calculating across projects, this is much more efficient than
# MAX(relative_position) without the GROUP BY, due to index usage:
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
relation = scoped_items
.order(Gitlab::Database.nulls_last_order('position', 'DESC'))
.group(self.class.relative_positioning_parent_column)
.limit(1)
relation = yield relation if block_given?
relation
.pluck(self.class.relative_positioning_parent_column, Arel.sql("#{calculation}(relative_position) AS position"))
.first&.last
# This method is used to exclude the current self (or another object)
# from a relation. Customize this if `id <> :id` is not sufficient
def exclude_self(relation, excluded: self)
relation.id_not_in(excluded.id)
end
def relative_siblings(relation = scoped_items)
relation.id_not_in(id)
end
def scoped_items
self.class.relative_positioning_query_base(self)
# Override if you want to be notified of failures to move
def could_not_move(exception)
end
end

View File

@ -444,20 +444,9 @@ class Issue < ApplicationRecord
Gitlab::EtagCaching::Store.new.touch(key)
end
def find_next_gap_before
super
rescue ActiveRecord::QueryCanceled => e
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
def find_next_gap_after
super
rescue ActiveRecord::QueryCanceled => e
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(nil, project_id)
raise e
end
end

View File

@ -41,10 +41,6 @@ class Project < ApplicationRecord
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
# nil: legacy
# 1: repository
# 2: attachments
LATEST_STORAGE_VERSION = 2
HASHED_STORAGE_FEATURES = {
repository: 1,

View File

@ -279,6 +279,7 @@ class User < ApplicationRecord
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:tab_width, :tab_width=,
:sourcegraph_enabled, :sourcegraph_enabled=,
:gitpod_enabled, :gitpod_enabled=,
:setup_for_company, :setup_for_company=,
:render_whitespace_in_code, :render_whitespace_in_code=,
:experience_level, :experience_level=,

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Placeholder class for model that is implemented in EE
# It reserves '+' as a reference prefix, but the table does not exist in FOSS
class Vulnerability < ApplicationRecord
include IgnorableColumns
def self.reference_prefix
'+'
end
def self.reference_prefix_escaped
'&plus;'
end
end
Vulnerability.prepend_if_ee('EE::Vulnerability')

View File

@ -2,6 +2,8 @@
module Ci
class CreateJobArtifactsService < ::BaseService
include Gitlab::Utils::UsageData
ArtifactsExistError = Class.new(StandardError)
LSIF_ARTIFACT_TYPE = 'lsif'
@ -22,7 +24,11 @@ module Ci
return result unless result[:status] == :success
headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type))
headers[:ProcessLsif] = lsif?(artifact_type)
if lsif?(artifact_type)
headers[:ProcessLsif] = true
track_usage_event('i_source_code_code_intelligence', project)
end
success(headers: headers)
end

View File

@ -0,0 +1,30 @@
- return unless Gitlab::Gitpod.feature_available?
- expanded = integration_expanded?('gitpod_')
- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
%section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Gitpod')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.form-check
= f.check_box :gitpod_enabled, class: 'form-check-input'
= f.label :gitpod_enabled, s_('Gitpod|Enable Gitpod integration'), class: 'form-check-label'
.form-group
= f.label :gitpod_url, s_('Gitpod|Gitpod URL'), class: 'label-bold'
= f.text_field :gitpod_url, class: 'form-control', placeholder: s_('Gitpod|e.g. https://gitpod.example.com')
.form-text.text-muted
= s_('Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects.')
= f.submit s_('Save changes'), class: 'btn btn-success'

View File

@ -117,6 +117,7 @@
#js-maintenance-mode-settings
= render_if_exists 'admin/application_settings/elasticsearch_form'
= render 'admin/application_settings/gitpod'
= render 'admin/application_settings/plantuml'
= render 'admin/application_settings/sourcegraph'
= render_if_exists 'admin/application_settings/slack'

View File

@ -23,10 +23,3 @@
= build.stage
%td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
%tr.build-log
- if build.has_trace?
%td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
%pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
= build.trace.html(last_lines: 30).html_safe
- else
%td{ colspan: "2" }

View File

@ -14,7 +14,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
<% if build.has_trace? -%>
Trace: <%= build.trace.raw(last_lines: 30) %>
<% end -%>
<% end -%>

View File

@ -34,8 +34,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
<% if build.has_trace? -%>
Trace: <%= build.trace.raw(last_lines: 30) %>
<% end -%>
<% end -%>

View File

@ -0,0 +1,11 @@
- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
%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
= s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }

View File

@ -0,0 +1,18 @@
- 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,26 +1,10 @@
- return unless Gitlab::Sourcegraph::feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
- sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
.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
%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
- link_start = '<a href="%{url}">'.html_safe % { url: sourcegraph_url }
- link_end = '</a>'.html_safe
= s_('Preferences|Enable integrated code intelligence on code views').html_safe % { link_start: link_start, link_end: link_end }
.form-text.text-muted
= sourcegraph_url_message
= sourcegraph_experimental_message
%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

@ -138,7 +138,7 @@
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
= render 'sourcegraph', f: f
= render 'integrations', f: f
.col-lg-4.profile-settings-sidebar
.col-lg-8

View File

@ -1,5 +1,6 @@
- can_collaborate = can_collaborate_with_project?(@project)
- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
- can_visit_ide = can_collaborate || current_user&.already_forked?(@project)
.tree-ref-container
.tree-ref-holder
@ -14,12 +15,12 @@
= render 'projects/find_file_link'
- if can_collaborate || current_user&.already_forked?(@project)
#js-tree-web-ide-link.d-inline-block
- elsif can_create_mr_from_fork
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if can_visit_ide || can_create_mr_from_fork
#js-tree-web-ide-link.d-inline-block{ data: { options: vue_ide_link_data(@project, @ref).to_json } }
- if !can_visit_ide
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- unless current_user&.gitpod_enabled
= render 'shared/gitpod/enable_gitpod_modal'
- if show_xcode_link?(@project)
.project-action-button.project-xcode.inline<

View File

@ -0,0 +1,12 @@
#modal-enable-gitpod.modal.qa-enable-gitpod-modal
.modal-dialog
.modal-content
.modal-header
%h3.page-title= _('Enable Gitpod?')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body.p-3
%p= (_("To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}.") % { user_prefs: link_to(_('user preferences'), profile_preferences_path(anchor: 'gitpod')) }).html_safe
.modal-footer
= link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
= link_to _('Enable Gitpod'), profile_path(user: { gitpod_enabled: true}), class: 'btn btn-success', method: :put

View File

@ -60,11 +60,11 @@ class FeatureFlagOptionParser
options.force = value
end
opts.on('-m', '--introduced-by-url [string]', String, 'URL to Merge Request introducing Feature Flag') do |value|
opts.on('-m', '--introduced-by-url [string]', String, 'URL of Merge Request introducing the Feature Flag') do |value|
options.introduced_by_url = value
end
opts.on('-i', '--rollout-issue-url [string]', String, 'URL to Issue rolling out Feature Flag') do |value|
opts.on('-i', '--rollout-issue-url [string]', String, 'URL of Issue rolling out the Feature Flag') do |value|
options.rollout_issue_url = value
end
@ -106,7 +106,7 @@ class FeatureFlagOptionParser
def read_group
$stdout.puts
$stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:"
$stdout.puts ">> Specify the group introducing the feature flag, like `group::apm`:"
loop do
$stdout.print "?> "
@ -114,7 +114,7 @@ class FeatureFlagOptionParser
group = nil if group.empty?
return group if group.nil? || group.start_with?('group::')
$stderr.puts "Group needs to include `group::`"
$stderr.puts "The group needs to include `group::`"
end
end
@ -123,7 +123,7 @@ class FeatureFlagOptionParser
return TYPES.first.first if TYPES.one?
$stdout.puts
$stdout.puts ">> Please specify the type of your feature flag:"
$stdout.puts ">> Specify the feature flag type:"
$stdout.puts
TYPES.each do |type, data|
$stdout.puts "#{type.to_s.rjust(15)}#{' '*6}#{data[:description]}"
@ -141,7 +141,7 @@ class FeatureFlagOptionParser
def read_introduced_by_url
$stdout.puts
$stdout.puts ">> If you have MR open, can you paste the URL here? (or enter to skip)"
$stdout.puts ">> URL of the MR introducing the feature flag (enter to skip):"
loop do
$stdout.print "?> "
@ -166,11 +166,11 @@ class FeatureFlagOptionParser
issue_new_url = url + "?" + URI.encode_www_form(params)
$stdout.puts
$stdout.puts ">> Open this URL and fill the rest of details:"
$stdout.puts ">> Open this URL and fill in the rest of the details:"
$stdout.puts issue_new_url
$stdout.puts
$stdout.puts ">> Paste URL of `rollout issue` here, or enter to skip:"
$stdout.puts ">> URL of the rollout issue (enter to skip):"
loop do
$stdout.print "?> "

View File

@ -0,0 +1,5 @@
---
title: Create placeholder model for Vulnerability to reserve + as a reference prefix
merge_request: 42147
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Track projects using code intelligence
merge_request: 41881
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix daemon memory killer jobs hash thread safety issue
merge_request: 42468
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Resolve Design comments do not render the blockquotes correctly
merge_request: 42498
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add Gitpod integration
merge_request: 37985
author: Cornelius Ludmann @corneliusludmann
type: added

View File

@ -0,0 +1,5 @@
---
title: Refactor relative positioning to enable better testing
merge_request: 41967
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove job logs from notification e-mails
merge_request: 42395
author:
type: changed

View File

@ -0,0 +1,7 @@
---
name: gitpod
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37985
rollout_issue_url:
group: group::editor
type: development
default_enabled: false

View File

@ -1,7 +1,7 @@
---
name: group_level_integrations
introduced_by_url:
rollout_issue_url:
group:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238575
group: group::ecosystem
type: development
default_enabled: false

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddGitpodApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20200727154631_add_gitpod_application_settings_text_limit
def change
add_column :application_settings, :gitpod_enabled, :boolean, default: false, null: false
add_column :application_settings, :gitpod_url, :text, default: 'https://gitpod.io/', null: true
end
# rubocop:enable Migration/AddLimitToTextColumns
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddGitpodApplicationSettingsTextLimit < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_text_limit :application_settings, :gitpod_url, 255
end
def down
remove_text_limit :application_settings, :gitpod_url
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddGitpodUserPreferences < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :user_preferences, :gitpod_enabled, :boolean, default: false, null: false
end
end

View File

@ -0,0 +1 @@
c04fe7e1a56bdcd41b5e1af346f9bfcae170d601954c4a0bcfcc9aea19d55528

View File

@ -0,0 +1 @@
0ce17a8ad6c5ca5bba49ff522fede400fe6666490157af123ad98a7643f3ce01

View File

@ -0,0 +1 @@
523f200c635e37ee1ac52257ffd45443a3e17bfe993d22775a5377865e044a46

View File

@ -9272,6 +9272,9 @@ CREATE TABLE public.application_settings (
enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
container_registry_delete_tags_service_timeout integer DEFAULT 250 NOT NULL,
elasticsearch_client_request_timeout integer DEFAULT 0 NOT NULL,
gitpod_enabled boolean DEFAULT false NOT NULL,
gitpod_url text DEFAULT 'https://gitpod.io/'::text,
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
@ -16277,7 +16280,8 @@ CREATE TABLE public.user_preferences (
tab_width smallint,
feature_filter_type bigint,
experience_level smallint,
view_diffs_file_by_file boolean DEFAULT false NOT NULL
view_diffs_file_by_file boolean DEFAULT false NOT NULL,
gitpod_enabled boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE public.user_preferences_id_seq

View File

@ -0,0 +1,269 @@
---
stage: Enablement
group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: howto
---
CAUTION: **Caution:**
This runbook is in **alpha**. For complete, production-ready documentation, see the
[disaster recovery documentation](index.md).
# Disaster Recovery (Geo) promotion runbooks **(PREMIUM ONLY)**
## Geo planned failover runbook 1
| Component | Configuration |
| ----------- | --------------- |
| PostgreSQL | Omnibus-managed |
| Geo site | Single-node |
| Secondaries | One |
This runbook will guide you through a planned failover of a single-node Geo site
with one secondary. The following general architecture is assumed:
```mermaid
graph TD
subgraph main[Geo deployment]
subgraph Primary[Primary site]
Node_1[(GitLab node)]
end
subgraph Secondary1[Secondary site]
Node_2[(GitLab node)]
end
end
```
This guide will result in the following:
1. An offline primary.
1. A promoted secondary that is now the new primary.
What is not covered:
1. Re-adding the old **primary** as a secondary.
1. Adding a new secondary.
### Preparation
NOTE: **Note:**
Before following any of those steps, make sure you have `root` access to the
**secondary** to promote it, since there isn't provided an automated way to
promote a Geo replica and perform a failover.
On the **secondary** node, navigate to the **Admin Area > Geo** dashboard to
review its status. Replicated objects (shown in green) should be close to 100%,
and there should be no failures (shown in red). If a large proportion of
objects aren't yet replicated (shown in gray), consider giving the node more
time to complete.
![Replication status](img/replication-status.png)
If any objects are failing to replicate, this should be investigated before
scheduling the maintenance window. After a planned failover, anything that
failed to replicate will be **lost**.
You can use the
[Geo status API](../../../api/geo_nodes.md#retrieve-project-sync-or-verification-failures-that-occurred-on-the-current-node)
to review failed objects and the reasons for failure.
A common cause of replication failures is the data being missing on the
**primary** node - you can resolve these failures by restoring the data from backup,
or removing references to the missing data.
The maintenance window won't end until Geo replication and verification is
completely finished. To keep the window as short as possible, you should
ensure these processes are close to 100% as possible during active use.
If the **secondary** node is still replicating data from the **primary** node,
follow these steps to avoid unnecessary data loss:
1. Until a [read-only mode](https://gitlab.com/gitlab-org/gitlab/-/issues/14609)
is implemented, updates must be prevented from happening manually to the
**primary**. Note that your **secondary** node still needs read-only
access to the **primary** node during the maintenance window:
1. At the scheduled time, using your cloud provider or your node's firewall, block
all HTTP, HTTPS and SSH traffic to/from the **primary** node, **except** for your IP and
the **secondary** node's IP.
For instance, you can run the following commands on the **primary** node:
```shell
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 22 -j ACCEPT
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 22 -j ACCEPT
sudo iptables -A INPUT --destination-port 22 -j REJECT
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 80 -j ACCEPT
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 80 -j ACCEPT
sudo iptables -A INPUT --tcp-dport 80 -j REJECT
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 443 -j ACCEPT
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 443 -j ACCEPT
sudo iptables -A INPUT --tcp-dport 443 -j REJECT
```
From this point, users will be unable to view their data or make changes on the
**primary** node. They will also be unable to log in to the **secondary** node.
However, existing sessions will work for the remainder of the maintenance period, and
public data will be accessible throughout.
1. Verify the **primary** node is blocked to HTTP traffic by visiting it in browser via
another IP. The server should refuse connection.
1. Verify the **primary** node is blocked to Git over SSH traffic by attempting to pull an
existing Git repository with an SSH remote URL. The server should refuse
connection.
1. On the **primary** node, disable non-Geo periodic background jobs by navigating
to **Admin Area > Monitoring > Background Jobs > Cron**, clicking `Disable All`,
and then clicking `Enable` for the `geo_sidekiq_cron_config_worker` cron job.
This job will re-enable several other cron jobs that are essential for planned
failover to complete successfully.
1. Finish replicating and verifying all data:
CAUTION: **Caution:**
Not all data is automatically replicated. Read more about
[what is excluded](planned_failover.md#not-all-data-is-automatically-replicated).
1. If you are manually replicating any
[data not managed by Geo](../replication/datatypes.md#limitations-on-replicationverification),
trigger the final replication process now.
1. On the **primary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
and wait for all queues except those with `geo` in the name to drop to 0.
These queues contain work that has been submitted by your users; failing over
before it is completed will cause the work to be lost.
1. On the **primary** node, navigate to **Admin Area > Geo** and wait for the
following conditions to be true of the **secondary** node you are failing over to:
- All replication meters to each 100% replicated, 0% failures.
- All verification meters reach 100% verified, 0% failures.
- Database replication lag is 0ms.
- The Geo log cursor is up to date (0 events behind).
1. On the **secondary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
and wait for all the `geo` queues to drop to 0 queued and 0 running jobs.
1. On the **secondary** node, use [these instructions](../../raketasks/check.md)
to verify the integrity of CI artifacts, LFS objects, and uploads in file
storage.
At this point, your **secondary** node will contain an up-to-date copy of everything the
**primary** node has, meaning nothing will be lost when you fail over.
1. In this final step, you need to permanently disable the **primary** node.
CAUTION: **Caution:**
When the **primary** node goes offline, there may be data saved on the **primary** node
that has not been replicated to the **secondary** node. This data should be treated
as lost if you proceed.
TIP: **Tip:**
If you plan to [update the **primary** domain DNS record](index.md#step-4-optional-updating-the-primary-domain-dns-record),
you may wish to lower the TTL now to speed up propagation.
When performing a failover, we want to avoid a split-brain situation where
writes can occur in two different GitLab instances. So to prepare for the
failover, you must disable the **primary** node:
- If you have SSH access to the **primary** node, stop and disable GitLab:
```shell
sudo gitlab-ctl stop
```
Prevent GitLab from starting up again if the server unexpectedly reboots:
```shell
sudo systemctl disable gitlab-runsvdir
```
NOTE: **Note:**
(**CentOS only**) In CentOS 6 or older, there is no easy way to prevent GitLab from being
started if the machine reboots isn't available (see [Omnibus GitLab issue #3058](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3058)).
It may be safest to uninstall the GitLab package completely with `sudo yum remove gitlab-ee`.
NOTE: **Note:**
(**Ubuntu 14.04 LTS**) If you are using an older version of Ubuntu
or any other distribution based on the Upstart init system, you can prevent GitLab
from starting if the machine reboots as `root` with
`initctl stop gitlab-runsvvdir && echo 'manual' > /etc/init/gitlab-runsvdir.override && initctl reload-configuration`.
- If you do not have SSH access to the **primary** node, take the machine offline and
prevent it from rebooting. Since there are many ways you may prefer to accomplish
this, we will avoid a single recommendation. You may need to:
- Reconfigure the load balancers.
- Change DNS records (for example, point the **primary** DNS record to the **secondary**
node in order to stop usage of the **primary** node).
- Stop the virtual servers.
- Block traffic through a firewall.
- Revoke object storage permissions from the **primary** node.
- Physically disconnect a machine.
### Promoting the **secondary** node
Note the following when promoting a secondary:
- A new **secondary** should not be added at this time. If you want to add a new
**secondary**, do this after you have completed the entire process of promoting
the **secondary** to the **primary**.
- If you encounter an `ActiveRecord::RecordInvalid: Validation failed: Name has already been taken`
error during this process, read
[the troubleshooting advice](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node).
To promote the secondary node:
1. SSH in to your **secondary** node and login as root:
```shell
sudo -i
```
1. Edit `/etc/gitlab/gitlab.rb` to reflect its new status as **primary** by
removing any lines that enabled the `geo_secondary_role`:
```ruby
## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
geo_secondary_role['enable'] = true
## In 11.5+ documentation, the role was enabled as follows. Remove this line.
roles ['geo_secondary_role']
```
1. Run the following command to list out all preflight checks and automatically
check if replication and verification are complete before scheduling a planned
failover to ensure the process will go smoothly:
```shell
gitlab-ctl promotion-preflight-checks
```
1. Promote the **secondary**:
```shell
gitlab-ctl promote-to-primary-node
```
If you have already run the [preflight checks](planned_failover.md#preflight-checks)
or don't want to run them, you can skip them:
```shell
gitlab-ctl promote-to-primary-node --skip-preflight-check
```
You can also promote the secondary node to primary **without any further confirmation**, even when preflight checks fail:
```shell
sudo gitlab-ctl promote-to-primary-node --force
```
1. Verify you can connect to the newly promoted **primary** node using the URL used
previously for the **secondary** node.
If successful, the **secondary** node has now been promoted to the **primary** node.
### Next steps
To regain geographic redundancy as quickly as possible, you should
[add a new **secondary** node](../setup/index.md). To
do that, you can re-add the old **primary** as a new secondary and bring it back
online.

View File

@ -1196,9 +1196,16 @@ CAUTION: **Caution:**
## Data recovery
If a Gitaly node fails replication jobs for any reason, it ends up hosting outdated versions of
the affected repositories. Praefect provides tools for automatically or manually reconciling
the outdated repositories in order to bring them fully up to date again.
If a Gitaly node fails replication jobs for any reason, it ends up hosting outdated versions of the
affected repositories. Praefect provides tools for:
- [Automatic](#automatic-reconciliation) reconciliation, for GitLab 13.4 and later.
- [Manual](#manual-reconciliation) reconciliation, for:
- GitLab 13.3 and earlier.
- Repositories upgraded to GitLab 13.4 and later without entries in the `repositories` table.
A migration tool [is planned](https://gitlab.com/gitlab-org/gitaly/-/issues/3033).
These tools reconcile the outdated repositories to bring them fully up to date again.
### Automatic reconciliation

View File

@ -2,8 +2,6 @@
## List users
Active users = Total accounts - Blocked users
Get a list of users.
This function takes pagination parameters `page` and `per_page` to restrict the list of users.
@ -49,9 +47,9 @@ For example:
GET /users?username=jack_smith
```
In addition, you can filter users based on states eg. `blocked`, `active`
This works only to filter users who are `blocked` or `active`.
It does not support `active=false` or `blocked=false`.
In addition, you can filter users based on the states `blocked` and `active`.
It does not support `active=false` or `blocked=false`. The list of active users
is the total number of users minus the blocked users.
```plaintext
GET /users?active=true

View File

@ -57,6 +57,7 @@ Complementary reads:
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLab team members)
- [Patch release process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/patch/process.md#process-for-developers)
- [Adding a new service component to GitLab](adding_service_component.md)
## UX and Frontend guides

View File

@ -0,0 +1,89 @@
# Adding a new Service Component to GitLab
The GitLab product is made up of several service components that run as independent system processes in communication with each other. These services can be run on the same instance, or spread across different instances. A list of the existing components can be found in the [GitLab architecture overview](architecture.md).
## Integration phases
The following outline re-uses the [maturity metric](https://about.gitlab.com/direction/maturity) naming as an example of the various phases of integrating a component. These are only loosely coupled to a components actual maturity, and are intended as a guide for implementation order (for example, a component does not need to be enabled by default to be Lovable, and being enabled by default does not on its own cause a component to be Lovable).
- Proposed
- [Proposing a new component](#proposing-a-new-component)
- Minimal
- [Integrating a new service with GitLab](#integrating-a-new-service-with-gitlab)
- [Handling service dependencies](#handling-service-dependencies)
- Viable
- [Bundled with GitLab installations](#bundling-a-service-with-gitlab)
- [End-to-end testing in GitLab QA](testing_guide/end_to_end/beginners_guide.md)
- [Release management](#release-management)
- [Enabled on GitLab.com](feature_flags/controls.md#enabling-a-feature-for-gitlabcom)
- Complete
- [Configurable by the GitLab orchestrator](https://gitlab.com/gitlab-org/gitlab-orchestrator)
- Lovable
- Enabled by default for the majority of users
## Proposing a new component
The initial step for integrating a new component with GitLab starts with creating a [Feature proposal in the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal).
Identify the [product category](https://about.gitlab.com/handbook/product/categories/) the component falls under and assign the Engineering Manager and Product Manager responsible for that category.
The general steps for getting any GitLab feature from proposal to release can be found in the [Product development flow](https://about.gitlab.com/handbook/product-development-flow/).
## Integrating a new service with GitLab
Adding a new service follows the same [merge request workflow](contributing/merge_request_workflow.md) as other contributions, and must meet the same [completion criteria](contributing/merge_request_workflow.md#definition-of-done) and in addition needs to cover the following:
- The [architecture component list](architecture.md#component-list) has been updated to include the service.
- Features provided by the component have been accepted into the [GitLab Product Direction](https://about.gitlab.com/direction/).
- Documentation is available and the support team has been made aware of the new component.
**For services that can operate completely separate from GitLab:**
The first iteration should be to add the ability to connect and use the service as an externally installed component. Often this involves providing settings in GitLab to connect to the service, or allow connections from it. And then shipping documentation on how to install and configure the service with GitLab.
TIP: **Tip:**
[Elasticsearch](../integration/elasticsearch.md#installing-elasticsearch) is an example of a service that has been integrated this way. And many of the other services, including internal projects like Gitaly, started off as separately installed alternatives.
**For services that depend on the existing GitLab codebase:**
The first iteration should be opt-in, either through the `gitlab.yml` configuration or through [feature flags](feature_flags.md). For these types of services it is often necessary to [bundle the service and its dependencies with GitLab](#bundling-a-service-with-gitlab) as part of the initial integration.
TIP: **Tip:**
[ActionCable](https://docs.gitlab.com/omnibus/settings/actioncable.html) is an example of a service that has been added this way.
## Bundling a service with GitLab
NOTE: **Note:**
Code shipped with GitLab needs to use a license approved by the Legal team. See the list of [existing approved licenses](https://about.gitlab.com/handbook/engineering/open-source/#using-open-source-libraries).
NOTE: **Note:**
Notify the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/) when adding a new dependency that must be compiled. We must be able to compile the dependency on all supported platforms.
New services to be bundled with GitLab need to be available in the following environments.
**Dev environment**
The first step of bundling a new service is to provide it in the development environment to engage in collaboration and feedback.
- [Include in the GDK](https://gitlab.com/gitlab-org/gitlab-development-kit)
- [Include in the source install instructions](../install/installation.md)
**Standard install methods**
In order for a service to be bundled for end-users or GitLab.com, it needs to be included in the standard install methods:
- [Included in the Omnibus package](https://gitlab.com/gitlab-org/omnibus-gitlab)
- [Included in the GitLab Helm charts](https://gitlab.com/gitlab-org/charts/gitlab)
## Handling service dependencies
Dependencies should be kept up to date and be tracked for security updates. For the Rails codebase, the JavaScript and Ruby dependencies are
scanned for vulnerabilities using GitLab [dependency scanning](../user/application_security/dependency_scanning/index.md).
In addition, any system dependencies used in Omnibus packages or the Cloud Native images should be added to the [dependency update automation](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/maintenance/dependencies.io.html#adding-new-dependencies).
## Release management
If the service component needs to be updated or released with the monthly GitLab release, then the component should be added to the [release tools automation](https://gitlab.com/gitlab-org/release-tools). This project is maintained by the [Delivery team](https://about.gitlab.com/handbook/engineering/infrastructure/team/delivery/). A list of the projects managed this way can be found in the [release tools project directory](https://about.gitlab.com/handbook/engineering/infrastructure/team/delivery/).
For example, during the monthly GitLab release, the desired version of Gitaly, GitLab Workhorse, GitLab Shell, etc., need to synchronized through the various release pipelines.

View File

@ -242,6 +242,7 @@ request:
1. The [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/prepare_build.sh).
1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab).
1. The [Cloud Native GitLab Dockerfiles](https://gitlab.com/gitlab-org/build/CNG)
## Incremental improvements

View File

@ -1120,16 +1120,14 @@ they need to interact with the application.
When you take screenshots:
- *Capture the most relevant area of the page.* Don't include unnecessary white
space or areas of the page that don't help illustrate your point. Also, don't
include the entire page if you don't have to, but also ensure the image
contains enough information to allow the user to determine where things are.
- *Be consistent.* Find a browser window size that works for you that also
displays all areas of the product, including the left navigation (usually >
1200px wide). For consistency, use this browser window size for your
screenshots by installing a browser extension for setting a window to a
specific size (for example,
[Window Resizer](https://chrome.google.com/webstore/detail/window-resizer/kkelicaakdanhinjdeammmilcgefonfh/related?hl=en)
for Google Chrome).
space or areas of the page that don't help illustrate the point. The left
sidebar of the GitLab user interface can change, so don't include the sidebar
if it's not necessary.
- *Keep it small.* If you don't need to show the full width of the screen, don't.
A value of 1000 pixels is a good maximum width for your screenshot image.
- *Be consistent.* Coordinate screenshots with the other screenshots already on
a documentation page. For example, if other screenshots include the left
sidebar, include the sidebar in all screenshots.
### Save the image

View File

@ -110,7 +110,7 @@ Each feature flag is defined in a separate YAML file consisting of a number of f
|---------------------|----------|----------------------------------------------------------------|
| `name` | yes | Name of the feature flag. |
| `type` | yes | Type of feature flag. |
| `default_enabled` | yes | The default state of the feature flag that is strongly validated, with `default_enabled:` passed as an argument. |
| `default_enabled` | yes | The default state of the feature flag that is strictly validated, with `default_enabled:` passed as an argument. |
| `introduced_by_url` | no | The URL to the Merge Request that introduced the feature flag. |
| `rollout_issue_url` | no | The URL to the Issue covering the feature flag rollout. |
| `group` | no | The [group](https://about.gitlab.com/handbook/product/product-categories/#devops-stages) that owns the feature flag. |
@ -129,16 +129,16 @@ Only feature flags that have a YAML definition file can be used when running the
```shell
$ bin/feature-flag my-feature-flag
>> Please specify the group introducing feature flag, like `group::apm`:
>> Specify the group introducing the feature flag, like `group::apm`:
?> group::memory
>> If you have MR open, can you paste the URL here? (or enter to skip)
>> URL of the MR introducing the feature flag (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
>> Open this URL and fill the rest of details:
>> Open this URL and fill in the rest of the details:
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out
>> Paste URL of `rollout issue` here, or enter to skip:
>> URL of the rollout issue (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533
create config/feature_flags/development/test-flag.yml
---
@ -305,7 +305,7 @@ used as an actor for `Feature.enabled?`.
### Feature flags for licensed features
If a feature is license-gated, there's no need to add an additional
explicit feature flag check since the flag will be checked as part of the
explicit feature flag check since the flag is checked as part of the
`License.feature_available?` call. Similarly, there's no need to "clean up" a
feature flag once the feature has reached general availability.
@ -316,7 +316,7 @@ a by default enabled feature flag with the same name as the provided argument.
**An important side-effect of the implicit feature flags mentioned above is that
unless the feature is explicitly disabled or limited to a percentage of users,
the feature flag check will default to `true`.**
the feature flag check defaults to `true`.**
NOTE: **Note:**
Due to limitations with `feature_available?`, the YAML definition for `licensed` feature
@ -361,9 +361,9 @@ default_enabled: [false, true]
Feature groups must be defined statically in `lib/feature.rb` (in the
`.register_feature_groups` method), but their implementation can obviously be
dynamic (querying the DB etc.).
dynamic (querying the DB, for example).
Once defined in `lib/feature.rb`, you will be able to activate a
Once defined in `lib/feature.rb`, you can to activate a
feature for a given feature group via the [`feature_group` parameter of the features API](../../api/features.md#set-or-create-a-feature)
### Enabling a feature flag locally (in development)
@ -374,7 +374,7 @@ In the rails console (`rails c`), enter the following command to enable a featur
Feature.enable(:feature_flag_name)
```
Similarly, the following command will disable a feature flag:
Similarly, the following command disables a feature flag:
```ruby
Feature.disable(:feature_flag_name)
@ -388,7 +388,7 @@ Feature.enable(:feature_flag_name, Project.find_by_full_path("root/my-project"))
## Feature flags in tests
Introducing a feature flag into the codebase creates an additional codepath that should be tested.
Introducing a feature flag into the codebase creates an additional code path that should be tested.
It is strongly advised to test all code affected by a feature flag, both when **enabled** and **disabled**
to ensure the feature works properly.
@ -423,10 +423,10 @@ Feature.enabled?(:ci_live_trace, project2) # => false
The behavior of FlipperGate is as follows:
1. You can enable an override for a specified actor to be enabled
1. You can enable an override for a specified actor to be enabled.
1. You can disable (remove) an override for a specified actor,
falling back to default state
1. There's no way to model that you explicitly disable a specified actor
falling back to the default state.
1. There's no way to model that you explicitly disabled a specified actor.
```ruby
Feature.enable(:my_feature)
@ -467,7 +467,7 @@ Feature.enable_percentage_of_time(:my_feature_3, 50)
Feature.enable_percentage_of_actors(:my_feature_4, 50)
```
Each feature flag that has a defined state will be persisted
Each feature flag that has a defined state is persisted
during test execution time:
```ruby

View File

@ -235,11 +235,10 @@ For example, to add support for files referenced by a `Widget` model with a
`ee/lib/gitlab/geo.rb`:
```ruby
def self.replicator_classes
classes = [::Geo::PackageFileReplicator,
::Geo::WidgetReplicator]
classes.select(&:enabled?)
REPLICATOR_CLASSES = [
::Geo::PackageFileReplicator,
::Geo::WidgetReplicator
]
end
```
@ -315,10 +314,6 @@ For example, to add support for files referenced by a `Widget` model with a
end
```
The method `has_create_events?` should return `true` in most of the cases.
However, if the entity you add doesn't have the create event, don't add the
method at all.
1. Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
1. Add `widget_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
@ -435,7 +430,7 @@ for verification state to the widgets table:
```
1. Add a partial index on `verification_failure` and `verification_checksum` to ensure
re-verification can be performed efficiently. Add a migration in `ee/db/geo/migrate/`:
re-verification can be performed efficiently:
```ruby
# frozen_string_literal: true
@ -461,9 +456,9 @@ for verification state to the widgets table:
##### Option 2: Create a separate `widget_states` table with verification state fields
1. Add a migration in `ee/db/geo/migrate/` to create a `widget_states` table and add a
partial index on `verification_failure` and `verification_checksum` to ensure
re-verification can be performed efficiently. Order the columns according to [our guidelines](../ordering_table_columns.md):
1. Create a `widget_states` table and add a partial index on `verification_failure` and
`verification_checksum` to ensure re-verification can be performed efficiently. Order
the columns according to [our guidelines](../ordering_table_columns.md):
```ruby
# frozen_string_literal: true

View File

@ -1,10 +1,19 @@
# Redis guidelines
GitLab uses [Redis](https://redis.io) for three distinct purposes:
GitLab uses [Redis](https://redis.io) for the following distinct purposes:
- Caching via `Rails.cache`.
- Caching (mostly via `Rails.cache`).
- As a job processing queue with [Sidekiq](sidekiq_style_guide.md).
- To manage the shared application state.
- As a Pub/Sub queue backend for ActionCable.
In most environments (including the GDK), all of these point to the same
Redis instance.
On GitLab.com, we use [separate Redis
instances](../administration/redis/replication_and_failover.md#running-multiple-redis-clusters).
(We do not currently use [ActionCable on
GitLab.com](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/228)).
Every application process is configured to use the same Redis servers, so they
can be used for inter-process communication in cases where [PostgreSQL](sql.md)
@ -21,11 +30,11 @@ to key names to avoid collisions. Typically we use colon-separated elements to
provide a semblance of structure at application level. An example might be
`projects:1:somekey`.
Although we split our Redis usage into three separate purposes, and those may
map to separate Redis servers in a [Highly Available](../administration/high_availability/redis.md)
configuration, the default Omnibus and GDK setups share a single Redis server.
This means that keys should **always** be globally unique across the three
purposes.
Although we split our Redis usage by purpose into distinct categories, and
those may map to separate Redis servers in a Highly Available
configuration like GitLab.com, the default Omnibus and GDK setups share
a single Redis server. This means that keys should **always** be
globally unique across all categories.
It is usually better to use immutable identifiers - project ID rather than
full path, for instance - in Redis key names. If full path is used, the key will
@ -56,3 +65,127 @@ Currently, we validate this in the development and test environments
with the [`RedisClusterValidator`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/instrumentation/redis_cluster_validator.rb),
which is enabled for the `cache` and `shared_state`
[Redis instances](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances)..
## Redis in structured logging
Our [structured logging](logging.md#use-structured-json-logging) for web
requests and Sidekiq jobs contains fields for the duration, call count,
bytes written, and bytes read per Redis instance, along with a total for
all Redis instances. For a particular request, this might look like:
| Field | Value |
| --- | --- |
| `json.queue_duration_s` | 0.01 |
| `json.redis_cache_calls` | 1 |
| `json.redis_cache_duration_s` | 0 |
| `json.redis_cache_read_bytes` | 109 |
| `json.redis_cache_write_bytes` | 49 |
| `json.redis_calls` | 2 |
| `json.redis_duration_s` | 0.001 |
| `json.redis_read_bytes` | 111 |
| `json.redis_shared_state_calls` | 1 |
| `json.redis_shared_state_duration_s` | 0 |
| `json.redis_shared_state_read_bytes` | 2 |
| `json.redis_shared_state_write_bytes` | 206 |
| `json.redis_write_bytes` | 255 |
As all of these fields are indexed, it is then straightforward to
investigate Redis usage in production. For instance, to find the
requests that read the most data from the cache, we can just sort by
`redis_cache_read_bytes` in descending order.
### The slow log
On GitLab.com, entries from the [Redis
slow log](https://redis.io/commands/slowlog) are available in the
`pubsub-redis-inf-gprd*` index with the [`redis.slowlog`
tag](https://log.gprd.gitlab.net/app/kibana#/discover?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-1d,to:now))&_a=(columns:!(json.type,json.command,json.exec_time),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:AWSQX_Vf93rHTYrsexmk,key:json.tag,negate:!f,params:(query:redis.slowlog),type:phrase),query:(match:(json.tag:(query:redis.slowlog,type:phrase))))),index:AWSQX_Vf93rHTYrsexmk)).
This shows commands that have taken a long time and may be a performance
concern.
The
[fluent-plugin-redis-slowlog](https://gitlab.com/gitlab-org/fluent-plugin-redis-slowlog)
project is responsible for taking the slowlog entries from Redis and
passing to fluentd (and ultimately Elasticsearch).
## Analyzing the entire keyspace
The [Redis Keyspace
Analyzer](https://gitlab.com/gitlab-com/gl-infra/redis-keyspace-analyzer)
project contains tools for dumping the full key list and memory usage of a Redis
instance, and then analyzing those lists while elimating potentially sensitive
data from the results. It can be used to find the most frequent key patterns, or
those that use the most memory.
Currently this is not run automatically for the GitLab.com Redis instances, but
is run manually on an as-needed basis.
## Utility classes
We have some extra classes to help with specific use cases. These are
mostly for fine-grained control of Redis usage, so they wouldn't be used
in combination with the `Rails.cache` wrapper: we'd either use
`Rails.cache` or these classes and literal Redis commands.
`Rails.cache` or these classes and literal Redis commands. We prefer
using `Rails.cache` so we can reap the benefits of future optimizations
done to Rails. It is worth noting that Ruby objects are
[marshalled](https://github.com/rails/rails/blob/v6.0.3.1/activesupport/lib/active_support/cache/redis_cache_store.rb#L447)
when written to Redis, so we need to pay attention to not to store huge
objects, or untrusted user input.
Typically we would only use these classes when at least one of the
following is true:
1. We want to manipulate data on a non-cache Redis instance.
1. `Rails.cache` does not support the operations we want to perform.
### `Gitlab::Redis::{Cache,SharedState,Queues}`
These classes wrap the Redis instances (using
[`Gitlab::Redis::Wrapper`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/wrapper.rb))
to make it convenient to work with them directly. The typical use is to
call `.with` on the class, which takes a block that yields the Redis
connection. For example:
```ruby
# Get the value of `key` from the shared state (persistent) Redis
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }
# Check if `value` is a member of the set `key`
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }
```
### `Gitlab::Redis::Boolean`
In Redis, every value is a string.
[`Gitlab::Redis::Boolean`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/boolean.rb)
makes sure that booleans are encoded and decoded consistently.
### `Gitlab::Redis::HLL`
The Redis [`PFCOUNT`](https://redis.io/commands/pfcount),
[`PFADD`](https://redis.io/commands/pfadd), and
[`PFMERGE`](https://redis.io/commands/pfmergge) commands operate on
HyperLogLogs, a data structure that allows estimating the number of unique
elements with low memory usage. (In addition to the `PFCOUNT` documentation,
Thoughtbot's article on [HyperLogLogs in
Redis](https://thoughtbot.com/blog/hyperloglogs-in-redis) provides a good
background here.)
[`Gitlab::Redis::HLL`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/hll.rb)
provides a convenient interface for adding and counting values in HyperLogLogs.
### `Gitlab::SetCache`
For cases where we need to efficiently check the whether an item is in a group
of items, we can use a Redis set.
[`Gitlab::SetCache`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/set_cache.rb)
provides an `#include?` method that will use the
[`SISMEMBER`](https://redis.io/commands/sismember) command, as well as `#read`
to fetch all entries in the set.
This is used by the
[`RepositorySetCache`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/repository_set_cache.rb)
to provide a convenient way to use sets to cache repository data like branch
names.

74
doc/integration/gitpod.md Normal file
View File

@ -0,0 +1,74 @@
---
type: reference, how-to
stage: Create
group: Editor
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
---
# Gitpod Integration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228893) in GitLab 13.4.
> - It's [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#configure-your-gitlab-instance-with-gitpod). **(CORE ONLY)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
With [Gitpod](https://gitpod.io/) you can describe your dev environment as code to get fully set
up, compiled, and tested dev environments for any GitLab project. The dev environments are not only
automated but also prebuilt which means that Gitpod continuously builds your Git branches like a CI
server. By that you dont have to wait for dependencies to be downloaded and builds to finish, but
you can start coding immediately.
In short: With Gitpod you can start coding instantly on any project, branch, and merge request from
any device, at any time.
![Gitpod interface](img/gitpod_web_interface_v13_4.png)
You can launch Gitpod directly from GitLab by clicking the **Gitpod** button from the **Web IDE**
dropdown on the project page:
![Gitpod Button on Project Page](img/gitpod_button_project_page_v13_4.png)
To learn more about Gitpod, see their [features](https://www.gitpod.io/features/) and
[documentation](https://www.gitpod.io/docs/).
To use the GitLab-Gitpod integration, you need to enable it from your user preferences:
1. From the GitLab UI, click your avatar in the top-right corner, then click **Settings**.
1. On the left-hand nav, click **Preferences**.
1. Under **Integrations**, find the **Gitpod** section.
1. Check **Enable Gitpod**.
Users of GitLab.com can enable it and start using straightaway. Users of GitLab self-managed instances
can follow the same steps once the integration has been enabled and configured by a GitLab administrator.
## Configure your GitLab instance with Gitpod **(CORE ONLY)**
If you are new to Gitpod, head over to the [Gitpod documentation](https://www.gitpod.io/docs/self-hosted/latest/self-hosted/)
and get your instance up and running.
1. In GitLab, go to **Admin Area > Settings > Integrations**.
1. Expand the **Gitpod** configuration section.
1. Check **Enable Gitpod**.
1. Add your Gitpod instance URL (for example, `https://gitpod.example.com`).
## Enable or disable the Gitpod integration **(CORE ONLY)**
The Gitpod integration is under development and not ready for production use. It is deployed behind a
feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:gitpod)
```
To disable it:
```ruby
Feature.disable(:gitpod)

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -64,10 +64,10 @@ Once a vulnerability is found, you can interact with it. Read more on how to
Please note that in some cases the reported vulnerabilities provide metadata that can contain
external links exposed in the UI. These links might not be accessible within an offline environment.
### Suggested Solutions for vulnerabilities
### Automatic remediation for vulnerabilities
The [suggested solutions](../index.md#solutions-for-vulnerabilities-auto-remediation) feature
(auto-remediation) is available for Dependency Scanning and Container Scanning, but may not work
The [automatic remediation for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation) feature
(auto-remediation) is available for offline Dependency Scanning and Container Scanning, but may not work
depending on your instance's configuration. We can only suggest solutions, which are generally more
current versions that have been patched, when we are able to access up-to-date registry services
hosting the latest versions of that dependency or image.

View File

@ -182,6 +182,12 @@ Manage the availability of integrated code intelligence features powered by
Sourcegraph. View [the Sourcegraph feature documentation](../../integration/sourcegraph.md#enable-sourcegraph-in-user-preferences)
for more information.
### Gitpod
Enable and disable the [GitLab-Gitpod integration](../../integration/gitpod.md). This is only
visible after the integration is configured by a GitLab administrator. View
[the Gitpod feature documentation](../../integration/gitpod.md) for more information.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -37,7 +37,12 @@ Import your projects from Bitbucket Server to GitLab with minimal effort.
empty changes.
1. Attachments in Markdown are currently not imported.
1. Task lists are not imported.
1. Emoji reactions are not imported
1. Emoji reactions are not imported.
1. [LFS objects](../../../topics/git/lfs/index.md) are not imported.
NOTE: **Note:**
To import a repository including LFS objects from a Bitbucket server repository, use the [Repo by URL](../import/repo_by_url.md) importer.
1. Project filtering does not support fuzzy search (only `starts with` or `full
match strings` are currently supported)

View File

@ -289,6 +289,17 @@ the command line.
NOTE: **Note:**
This section might move in its own document in the future.
### Copy the branch name for local checkout
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23767) in GitLab 13.4.
The merge request sidebar contains the branch reference for the source branch
used to contribute changes for this merge request.
To copy the branch reference into your clipboard, click the **Copy branch name** button
(**{copy-to-clipboard}**) in the right sidebar. You can then use it to checkout the branch locally
via command line by running `git checkout <branch-name>`.
### Checkout merge requests locally through the `head` ref
A merge request contains all the history from a repository, plus the additional

View File

@ -15,10 +15,7 @@ type: reference, howto
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can [disable it](#enable-or-disable-project-access-tokens).
Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens).
<!-- Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed -->
<!-- You can also use project access tokens with Git to authenticate over HTTP or SSH. -->
Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens). You can also use project access tokens with Git to authenticate over HTTP or SSH.
Project access tokens expire on the date you define, at midnight UTC.

View File

@ -53,21 +53,42 @@ If you are missing Syntax Highlighting support for any language, we prepared a s
NOTE: **Note:**
Single file editing is based on the [Ace Editor](https://ace.c9.io).
### Schema based validation
### Themes
> - Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.
> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1.
All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor.
You can pick a theme from your [profile preferences](../../profile/preferences.md).
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
which apply to the entire Web IDE screen.
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------|
| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) |
## Schema based validation
> - Support for validation based on predefined schemas [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
> - It was deployed behind a feature flag, disabled by default.
> - It's enabled on GitLab.com.
> - It cannot be enabled or disabled per-project.
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-schema-based-validation).
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-validation-based-on-predefined-schemas).
> - Support for validation based on custom schemas [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/226982) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
The Web IDE provides validation support for certain JSON and YAML files using schemas
based on the [JSON Schema Store](https://www.schemastore.org/json/). This feature is
only supported for the `.gitlab-ci.yml` file.
based on the [JSON Schema Store](https://www.schemastore.org/json/).
#### Enable or disable Schema based validation **(CORE ONLY)**
### Predefined schemas
Schema based validation is under development and not ready for production use. It is
The Web IDE has validation for certain files built in. This feature is only supported for
the `*.gitlab-ci.yml` files.
#### Enable or disable validation based on predefined schemas **(CORE ONLY)**
Validation based on predefined schemas is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default** for self-managed instances,
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance.
@ -84,21 +105,35 @@ To disable it:
Feature.disable(:schema_linting)
```
### Themes
### Custom schemas **(PREMIUM)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.
> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/226982) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor.
You can pick a theme from your [profile preferences](../../profile/preferences.md).
The Web IDE also allows you to define custom schemas for certain JSON/YAML files in your project.
You can do so by defining a `schemas` entry in the `.gitlab/.gitlab-webide.yml` file inside the
repository's root. Here is an example configuration:
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
which apply to the entire Web IDE screen.
```yaml
schemas:
- uri: https://json.schemastore.org/package
match:
- package.json
- uri: https://somewebsite.com/first/raw/url
match:
- data/release_posts/unreleased/*.{yml,yaml}
- uri: https://somewebsite.com/second/raw/url
match:
- "*.meta.json"
```
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------|
| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) |
Each schema entry supports two properties:
- `uri`: please provide an absolute URL for the schema definition file here. The schema from this URL
is loaded when a matching file is open.
- `match`: a list of matching paths or glob expressions. If a schema matches a particular path pattern,
it will be applied to that file. Please enclose the pattern in quotes if it begins with an asterisk (`*`),
it's be applied to that file. If a pattern begins with an asterisk (`*`), enclose it in quotation
marks. Otherwise, the configuration file is not valid YAML.
## Configure the Web IDE

View File

@ -61,6 +61,10 @@ module API
end
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitpod_enabled, type: Boolean, desc: 'Enable Gitpod'
given gitpod_enabled: ->(val) { val } do
requires :gitpod_url, type: String, desc: 'The configured Gitpod instance URL'
end
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@ -150,6 +154,7 @@ module API
end
optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes"
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",

View File

@ -11,9 +11,11 @@ module Gitlab
class SetNullPackageFilesFileStoreToLocalValue
LOCAL_STORE = 1 # equal to ObjectStorage::Store::LOCAL
# Temporary AR class for package files
class PackageFile < ActiveRecord::Base
self.table_name = 'packages_package_files'
module Packages
# Temporary AR class for package files
class PackageFile < ActiveRecord::Base
self.table_name = 'packages_package_files'
end
end
def perform(start_id, stop_id)

View File

@ -6,17 +6,21 @@ module Gitlab
class Metrics
extend Gitlab::Utils::StrongMemoize
OPERATIONS = [:appended, :mutated, :overwrite, :accepted,
:finalized, :discarded, :flaky].freeze
OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite,
:accepted, :finalized, :discarded, :conflict].freeze
def increment_trace_operation(operation: :unknown)
unless OPERATIONS.include?(operation)
raise ArgumentError, 'unknown trace operation'
raise ArgumentError, "unknown trace operation: #{operation}"
end
self.class.trace_operations.increment(operation: operation)
end
def increment_trace_bytes(size)
self.class.trace_bytes.increment(by: size.to_i)
end
def self.trace_operations
strong_memoize(:trace_operations) do
name = :gitlab_ci_trace_operations_total
@ -25,6 +29,15 @@ module Gitlab
Gitlab::Metrics.counter(name, comment)
end
end
def self.trace_bytes
strong_memoize(:trace_bytes) do
name = :gitlab_ci_trace_bytes_total
comment = 'Total amount of build trace bytes transferred'
Gitlab::Metrics.counter(name, comment)
end
end
end
end
end

View File

@ -8,7 +8,7 @@ module Gitlab
BUFFER_SIZE = 4096
LIMIT_SIZE = 500.kilobytes
attr_reader :stream
attr_reader :stream, :metrics
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
@ -16,9 +16,10 @@ module Gitlab
alias_method :present?, :valid?
def initialize
def initialize(metrics = Trace::Metrics.new)
@stream = yield
@stream&.binmode
@metrics = metrics
end
def valid?
@ -43,6 +44,9 @@ module Gitlab
def append(data, offset)
data = data.force_encoding(Encoding::BINARY)
metrics.increment_trace_operation(operation: :streamed)
metrics.increment_trace_bytes(data.bytesize)
stream.seek(offset, IO::SEEK_SET)
stream.write(data)
stream.truncate(offset + data.bytesize)

View File

@ -35,19 +35,12 @@ module Gitlab
[service_address, service_port]
end
def discover_prometheus_uri
def discover_prometheus_server_address
service_address, service_port = discover_service(service_name: 'prometheus')
return unless service_address && service_port
# There really is not a way to discover whether a Prometheus connection is using TLS or not
# Try TLS first because HTTPS will return fast if failed.
%w[https http].find do |scheme|
connection_url = "#{scheme}://#{service_address}:#{service_port}"
break connection_url if Gitlab::PrometheusClient.new(connection_url, allow_local_requests: true).healthy?
rescue
nil
end
"#{service_address}:#{service_port}"
end
private

30
lib/gitlab/gitpod.rb Normal file
View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Gitlab
class Gitpod
class << self
def feature_conditional?
feature.conditional?
end
def feature_available?
# The gitpod_bundle feature could be conditionally applied, so check if `!off?`
!feature.off?
end
def feature_enabled?(actor = nil)
feature.enabled?(actor)
end
def feature_and_settings_enabled?(actor = nil)
feature_enabled?(actor) && Gitlab::CurrentSettings.gitpod_enabled
end
private
def feature
Feature.get(:gitpod) # rubocop:disable Gitlab/AvoidFeatureGet
end
end
end
end

View File

@ -25,6 +25,10 @@ module Gitlab
end
end
def self.server_address
uri&.strip&.sub(/^http[s]?:\/\//, '')
end
def self.listen_address
Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
rescue Settingslogic::MissingSetting

View File

@ -42,6 +42,15 @@ module Gitlab
response_body == HEALTHY_RESPONSE
end
def ready?
response = get(ready_url, {})
# From Prometheus docs: This endpoint returns 200 when Prometheus is ready to serve traffic (i.e. respond to queries).
response.code == 200
rescue => e
raise PrometheusClient::UnexpectedResponseError, "#{e.message}"
end
def proxy(type, args)
path = api_path(type)
get(path, args)
@ -103,7 +112,11 @@ module Gitlab
end
def health_url
[api_url, '-/healthy'].join('/')
"#{api_url}/-/healthy"
end
def ready_url
"#{api_url}/-/ready"
end
private

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gitlab
module RelativePositioning
STEPS = 10
IDEAL_DISTANCE = 2**(STEPS - 1) + 1
MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
START_POSITION = 0
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
MAX_GAP = IDEAL_DISTANCE * 2
MIN_GAP = 2
NoSpaceLeft = Class.new(StandardError)
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
#
module Gitlab
module RelativePositioning
class Gap
attr_reader :start_pos, :end_pos
def initialize(start_pos, end_pos)
@start_pos, @end_pos = start_pos, end_pos
end
def ==(other)
other.is_a?(self.class) && other.start_pos == start_pos && other.end_pos == end_pos
end
def delta
((start_pos - end_pos) / 2.0).abs.ceil.clamp(0, RelativePositioning::IDEAL_DISTANCE)
end
end
end
end

View File

@ -0,0 +1,259 @@
# frozen_string_literal: true
module Gitlab
module RelativePositioning
# This class is API private - it should not be explicitly instantiated
# outside of tests
# rubocop: disable CodeReuse/ActiveRecord
class ItemContext
include Gitlab::Utils::StrongMemoize
attr_reader :object, :model_class, :range
attr_accessor :ignoring
def initialize(object, range, ignoring: nil)
@object = object
@range = range
@model_class = object.class
@ignoring = ignoring
end
def ==(other)
other.is_a?(self.class) && other.object == object && other.range == range && other.ignoring == ignoring
end
def positioned?
relative_position.present?
end
def min_relative_position
strong_memoize(:min_relative_position) { calculate_relative_position('MIN') }
end
def max_relative_position
strong_memoize(:max_relative_position) { calculate_relative_position('MAX') }
end
def prev_relative_position
calculate_relative_position('MAX') { |r| nextify(r, false) } if object.relative_position
end
def next_relative_position
calculate_relative_position('MIN') { |r| nextify(r) } if object.relative_position
end
def nextify(relation, gt = true)
if gt
relation.where("relative_position > ?", relative_position)
else
relation.where("relative_position < ?", relative_position)
end
end
def relative_siblings(relation = scoped_items)
object.exclude_self(relation)
end
# Handles the possibility that the position is already occupied by a sibling
def place_at_position(position, lhs)
current_occupant = relative_siblings.find_by(relative_position: position)
if current_occupant.present?
Mover.new(position, range).move(object, lhs.object, current_occupant)
else
object.relative_position = position
end
end
def lhs_neighbour
scoped_items
.where('relative_position < ?', relative_position)
.reorder(relative_position: :desc)
.first
.then { |x| neighbour(x) }
end
def rhs_neighbour
scoped_items
.where('relative_position > ?', relative_position)
.reorder(relative_position: :asc)
.first
.then { |x| neighbour(x) }
end
def neighbour(item)
return unless item.present?
self.class.new(item, range, ignoring: ignoring)
end
def scoped_items
r = model_class.relative_positioning_query_base(object)
r = object.exclude_self(r, excluded: ignoring) if ignoring.present?
r
end
def calculate_relative_position(calculation)
# When calculating across projects, this is much more efficient than
# MAX(relative_position) without the GROUP BY, due to index usage:
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
relation = scoped_items
.order(Gitlab::Database.nulls_last_order('position', 'DESC'))
.group(grouping_column)
.limit(1)
relation = yield relation if block_given?
relation
.pluck(grouping_column, Arel.sql("#{calculation}(relative_position) AS position"))
.first&.last
end
def grouping_column
model_class.relative_positioning_parent_column
end
def max_sibling
sib = relative_siblings
.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC'))
.first
neighbour(sib)
end
def min_sibling
sib = relative_siblings
.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
.first
neighbour(sib)
end
def shift_left
move_sequence_before(true)
object.reset
end
def shift_right
move_sequence_after(true)
object.reset
end
def create_space_left
find_next_gap_before.tap { |gap| move_sequence_before(false, next_gap: gap) }
end
def create_space_right
find_next_gap_after.tap { |gap| move_sequence_after(false, next_gap: gap) }
end
def find_next_gap_before
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
.where('relative_position <= ?', relative_position)
.order(relative_position: :desc)
find_next_gap(items_with_next_pos, range.first)
end
def find_next_gap_after
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
.where('relative_position >= ?', relative_position)
.order(:relative_position)
find_next_gap(items_with_next_pos, range.last)
end
def find_next_gap(items_with_next_pos, default_end)
gap = model_class
.from(items_with_next_pos, :items)
.where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
.limit(1)
.pluck(:pos, :next_pos)
.first
return if gap.nil? || gap.first == default_end
Gap.new(gap.first, gap.second || default_end)
end
def relative_position
object.relative_position
end
private
# Moves the sequence before the current item to the middle of the next gap
# For example, we have
#
# 5 . . . . . 11 12 13 14 [15] 16 . 17
# -----------
#
# This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
#
# 5 . . 8 9 10 11 . . . [15] 16 . 17
# ---------
#
# Creating a gap to the left of the current item. We can understand this as
# dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the right of the current item:
#
# 5 . . 8 9 10 11 [14] . . . 16 . 17
# --------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
raise NoSpaceLeft unless next_gap.present?
delta = next_gap.delta
move_sequence(next_gap.start_pos, relative_position, -delta, include_self)
end
# Moves the sequence after the current item to the middle of the next gap
# For example, we have:
#
# 8 . 10 [11] 12 13 14 15 . . . . . 21
# -----------
#
# This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
#
# 8 . 10 [11] . . . 15 16 17 18 . . 21
# -----------
#
# Creating a gap to the right of the current item. We can understand this as
# dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
#
# If `include_self` is true, the current item will also be moved, creating a
# gap to the left of the current item:
#
# 8 . 10 . . . [14] 15 16 17 18 . . 21
# ----------------
#
# As an optimization, the gap can be precalculated and passed to this method.
#
# @api private
# @raises NoSpaceLeft if the sequence cannot be moved
def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
raise NoSpaceLeft unless next_gap.present?
delta = next_gap.delta
move_sequence(relative_position, next_gap.start_pos, delta, include_self)
end
def move_sequence(start_pos, end_pos, delta, include_self = false)
relation = include_self ? scoped_items : relative_siblings
object.update_relative_siblings(relation, (start_pos..end_pos), delta)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -0,0 +1,155 @@
# frozen_string_literal: true
module Gitlab
module RelativePositioning
class Mover
attr_reader :range, :start_position
def initialize(start, range)
@range = range
@start_position = start
end
def move_to_end(object)
focus = context(object, ignoring: object)
max_pos = focus.max_relative_position
move_to_range_end(focus, max_pos)
end
def move_to_start(object)
focus = context(object, ignoring: object)
min_pos = focus.min_relative_position
move_to_range_start(focus, min_pos)
end
def move(object, first, last)
raise ArgumentError, 'object is required' unless object
lhs = context(first, ignoring: object)
rhs = context(last, ignoring: object)
focus = context(object)
range = RelativePositioning.range(lhs, rhs)
if range.cover?(focus)
# Moving a object already within a range is a no-op
elsif range.open_on_left?
move_to_range_start(focus, range.rhs.relative_position)
elsif range.open_on_right?
move_to_range_end(focus, range.lhs.relative_position)
else
pos_left, pos_right = create_space_between(range)
desired_position = position_between(pos_left, pos_right)
focus.place_at_position(desired_position, range.lhs)
end
end
def context(object, ignoring: nil)
return unless object
ItemContext.new(object, range, ignoring: ignoring)
end
private
def gap_too_small?(pos_a, pos_b)
return false unless pos_a && pos_b
(pos_a - pos_b).abs < MIN_GAP
end
def move_to_range_end(context, max_pos)
range_end = range.last + 1
new_pos = if max_pos.nil?
start_position
elsif gap_too_small?(max_pos, range_end)
max = context.max_sibling
max.ignoring = context.object
max.shift_left
position_between(max.relative_position, range_end)
else
position_between(max_pos, range_end)
end
context.object.relative_position = new_pos
end
def move_to_range_start(context, min_pos)
range_end = range.first - 1
new_pos = if min_pos.nil?
start_position
elsif gap_too_small?(min_pos, range_end)
sib = context.min_sibling
sib.ignoring = context.object
sib.shift_right
position_between(sib.relative_position, range_end)
else
position_between(min_pos, range_end)
end
context.object.relative_position = new_pos
end
def create_space_between(range)
pos_left = range.lhs&.relative_position
pos_right = range.rhs&.relative_position
return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right)
gap = range.rhs.create_space_left
[pos_left - gap.delta, pos_right]
rescue NoSpaceLeft
gap = range.lhs.create_space_right
[pos_left, pos_right + gap.delta]
end
# This method takes two integer values (positions) and
# calculates the position between them. The range is huge as
# the maximum integer value is 2147483647.
#
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
#
# Then we handle one of three cases:
# - If the gap is too small, we raise NoSpaceLeft
# - If the gap is larger than MAX_GAP, we place the new position at most
# IDEAL_DISTANCE from the edge of the gap.
# - otherwise we place the new position at the midpoint.
#
# The new position will always satisfy: pos_before <= midpoint <= pos_after
#
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
# If the gap is too small, NoSpaceLeft is raised.
#
# @raises NoSpaceLeft
def position_between(pos_before, pos_after)
pos_before ||= range.first
pos_after ||= range.last
pos_before, pos_after = [pos_before, pos_after].sort
gap_width = pos_after - pos_before
if gap_too_small?(pos_before, pos_after)
raise NoSpaceLeft
elsif gap_width > MAX_GAP
if pos_before <= range.first
pos_after - IDEAL_DISTANCE
elsif pos_after >= range.last
pos_before + IDEAL_DISTANCE
else
midpoint(pos_before, pos_after)
end
else
midpoint(pos_before, pos_after)
end
end
def midpoint(lower_bound, upper_bound)
((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1)
end
end
end
end

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
module Gitlab
module RelativePositioning
IllegalRange = Class.new(ArgumentError)
class Range
attr_reader :lhs, :rhs
def open_on_left?
lhs.nil?
end
def open_on_right?
rhs.nil?
end
def cover?(item_context)
return false unless item_context
return false unless item_context.positioned?
return true if item_context.object == lhs&.object
return true if item_context.object == rhs&.object
pos = item_context.relative_position
return lhs.relative_position < pos if open_on_right?
return pos < rhs.relative_position if open_on_left?
lhs.relative_position < pos && pos < rhs.relative_position
end
def ==(other)
other.is_a?(RelativePositioning::Range) && lhs == other.lhs && rhs == other.rhs
end
end
def self.range(lhs, rhs)
if lhs && rhs
ClosedRange.new(lhs, rhs)
elsif lhs
StartingFrom.new(lhs)
elsif rhs
EndingAt.new(rhs)
else
raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs
end
end
class ClosedRange < RelativePositioning::Range
def initialize(lhs, rhs)
@lhs, @rhs = lhs, rhs
raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs
raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs
end
end
class StartingFrom < RelativePositioning::Range
include Gitlab::Utils::StrongMemoize
def initialize(lhs)
@lhs = lhs
raise IllegalRange, 'lhs is required' unless lhs
end
def rhs
strong_memoize(:rhs) { lhs.rhs_neighbour }
end
end
class EndingAt < RelativePositioning::Range
include Gitlab::Utils::StrongMemoize
def initialize(rhs)
@rhs = rhs
raise IllegalRange, 'rhs is required' unless rhs
end
def lhs
strong_memoize(:lhs) { rhs.lhs_neighbour }
end
end
end
end

View File

@ -230,8 +230,10 @@ module Gitlab
end
def rss_increase_by_jobs
Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
rss_increase_by_job(job)
Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do
Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
rss_increase_by_job(job)
end
end
end

View File

@ -12,8 +12,8 @@ module Gitlab
!feature.off?
end
def feature_enabled?(thing = nil)
feature.enabled?(thing)
def feature_enabled?(actor = nil)
feature.enabled?(actor)
end
private

View File

@ -815,11 +815,9 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
# rubocop: disable UsageData/DistinctCountByLargeForeignKey
def cluster_applications_user_distinct_count(applications, time_period)
distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id')
end
# rubocop: enable UsageData/DistinctCountByLargeForeignKey
def clusters_user_distinct_count(clusters, time_period)
distinct_count(clusters.where(time_period), :user_id)

View File

@ -40,7 +40,7 @@ module Gitlab
private
def topology_fetch_all_data
with_prometheus_client(fallback: {}) do |client|
with_prometheus_client(fallback: {}, verify: false) do |client|
{
application_requests_per_hour: topology_app_requests_per_hour(client),
query_apdex_weekly_average: topology_query_apdex_weekly_average(client),

View File

@ -120,6 +120,10 @@
- name: merge_request_action
category: source_code
aggregation: daily
- name: i_source_code_code_intelligence
redis_slot: source_code
category: source_code
aggregation: daily
# Incident management
- name: incident_management_alert_status_changed
redis_slot: incident_management

View File

@ -83,11 +83,11 @@ module Gitlab
end
end
def with_prometheus_client(fallback: nil)
api_url = prometheus_api_url
return fallback unless api_url
def with_prometheus_client(fallback: nil, verify: true)
client = prometheus_client(verify: verify)
return fallback unless client
yield Gitlab::PrometheusClient.new(api_url, allow_local_requests: true)
yield client
end
def measure_duration
@ -111,11 +111,27 @@ module Gitlab
private
def prometheus_api_url
def prometheus_client(verify:)
server_address = prometheus_server_address
return unless server_address
# There really is not a way to discover whether a Prometheus connection is using TLS or not
# Try TLS first because HTTPS will return fast if failed.
%w[https http].find do |scheme|
api_url = "#{scheme}://#{server_address}"
client = Gitlab::PrometheusClient.new(api_url, allow_local_requests: true, verify: verify)
break client if client.ready?
rescue
nil
end
end
def prometheus_server_address
if Gitlab::Prometheus::Internal.prometheus_enabled?
Gitlab::Prometheus::Internal.uri
Gitlab::Prometheus::Internal.server_address
elsif Gitlab::Consul::Internal.api_url
Gitlab::Consul::Internal.discover_prometheus_uri
Gitlab::Consul::Internal.discover_prometheus_server_address
end
end

View File

@ -9240,9 +9240,6 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
msgid "Edit fork in Web IDE"
msgstr ""
msgid "Edit group: %{group_name}"
msgstr ""
@ -9420,9 +9417,18 @@ msgstr ""
msgid "Enable"
msgstr ""
msgid "Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab."
msgstr ""
msgid "Enable Auto DevOps"
msgstr ""
msgid "Enable Gitpod"
msgstr ""
msgid "Enable Gitpod?"
msgstr ""
msgid "Enable HTML emails"
msgstr ""
@ -11996,6 +12002,21 @@ msgstr ""
msgid "Gitlab Pages"
msgstr ""
msgid "Gitpod"
msgstr ""
msgid "Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects."
msgstr ""
msgid "Gitpod|Enable Gitpod integration"
msgstr ""
msgid "Gitpod|Gitpod URL"
msgstr ""
msgid "Gitpod|e.g. https://gitpod.example.com"
msgstr ""
msgid "Given access %{time_ago}"
msgstr ""
@ -14594,6 +14615,9 @@ msgstr ""
msgid "Latest pipeline for the most recent commit on this branch"
msgstr ""
msgid "Launch a ready-to-code development environment for your project."
msgstr ""
msgid "Lead"
msgstr ""
@ -20679,6 +20703,9 @@ msgstr ""
msgid "Quick range"
msgstr ""
msgid "Quickly and easily edit multiple files in your project."
msgstr ""
msgid "README"
msgstr ""
@ -26561,6 +26588,9 @@ msgstr ""
msgid "To update Snippets with multiple files, you must use the `files` parameter"
msgstr ""
msgid "To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}."
msgstr ""
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr ""
@ -30892,6 +30922,9 @@ msgstr ""
msgid "user avatar"
msgstr ""
msgid "user preferences"
msgstr ""
msgid "username"
msgstr ""

View File

@ -3,8 +3,8 @@ LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
ENV DEBIAN_FRONTEND="noninteractive"
ENV DOCKER_VERSION="17.09.0-ce"
ENV CHROME_VERSION="85.0.4183.83-1"
ENV CHROME_DRIVER_VERSION="85.0.4183.87"
ENV CHROME_VERSION="84.0.4147.89-1"
ENV CHROME_DRIVER_VERSION="84.0.4147.30"
ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}"

View File

@ -56,7 +56,7 @@ module QA
element :new_file_option
end
view 'app/assets/javascripts/repository/components/web_ide_link.vue' do
view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do
element :web_ide_button
end

View File

@ -31,11 +31,11 @@ module RuboCop
private
def allowed_foreign_key?(key)
[:sym, :str].include?(key.type) && allowed_foreign_keys.include?(key.value.to_sym)
[:sym, :str].include?(key.type) && allowed_foreign_keys.include?(key.value.to_s)
end
def allowed_foreign_keys
cop_config['AllowedForeignKeys'] || []
(cop_config['AllowedForeignKeys'] || []).map(&:to_s)
end
end
end

View File

@ -38,12 +38,13 @@ UsageData/DistinctCountByLargeForeignKey:
- 'lib/gitlab/usage_data.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
AllowedForeignKeys:
- :user_id
- :author_id
- :creator_id
- :owner_id
- :project_id
- :issue_id
- :merge_request_id
- :merge_requests.target_project_id
- :agent_id
- 'agent_id'
- 'author_id'
- 'clusters.user_id'
- 'creator_id'
- 'issue_id'
- 'merge_request_id'
- 'merge_requests.target_project_id'
- 'owner_id'
- 'project_id'
- 'user_id'

View File

@ -135,7 +135,7 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_return(type)
expect do
expect(described_class.read_type).to eq(:development)
end.to output(/specify the type/).to_stdout
end.to output(/Specify the feature flag type/).to_stdout
end
context 'when invalid type is given' do
@ -147,7 +147,7 @@ RSpec.describe 'bin/feature-flag' do
expect do
expect { described_class.read_type }.to raise_error(/EOF/)
end.to output(/specify the type/).to_stdout
end.to output(/Specify the feature flag type/).to_stdout
.and output(/Invalid type specified/).to_stderr
end
end
@ -161,7 +161,7 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_return(group)
expect do
expect(described_class.read_group).to eq('group::memory')
end.to output(/specify the group/).to_stdout
end.to output(/Specify the group introducing the feature flag/).to_stdout
end
context 'invalid group given' do
@ -173,8 +173,8 @@ RSpec.describe 'bin/feature-flag' do
expect do
expect { described_class.read_group }.to raise_error(/EOF/)
end.to output(/specify the group/).to_stdout
.and output(/Group needs to include/).to_stderr
end.to output(/Specify the group introducing the feature flag/).to_stdout
.and output(/The group needs to include/).to_stderr
end
end
end
@ -186,7 +186,7 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to eq('https://merge-request')
end.to output(/can you paste the URL here/).to_stdout
end.to output(/URL of the MR introducing the feature flag/).to_stdout
end
context 'empty URL given' do
@ -196,7 +196,7 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_introduced_by_url).to be_nil
end.to output(/can you paste the URL here/).to_stdout
end.to output(/URL of the MR introducing the feature flag/).to_stdout
end
end
@ -209,7 +209,7 @@ RSpec.describe 'bin/feature-flag' do
expect do
expect { described_class.read_introduced_by_url }.to raise_error(/EOF/)
end.to output(/can you paste the URL here/).to_stdout
end.to output(/URL of the MR introducing the feature flag/).to_stdout
.and output(/URL needs to start with/).to_stderr
end
end
@ -223,7 +223,7 @@ RSpec.describe 'bin/feature-flag' do
expect($stdin).to receive(:gets).and_return(url)
expect do
expect(described_class.read_rollout_issue_url(options)).to eq('https://issue')
end.to output(/Paste URL of `rollout issue` here/).to_stdout
end.to output(/URL of the rollout issue/).to_stdout
end
context 'invalid URL given' do
@ -235,7 +235,7 @@ RSpec.describe 'bin/feature-flag' do
expect do
expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
end.to output(/Paste URL of `rollout issue` here/).to_stdout
end.to output(/URL of the rollout issue/).to_stdout
.and output(/URL needs to start/).to_stderr
end
end

View File

@ -17,7 +17,10 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
end
context 'General page' do
let(:gitpod_feature_enabled) { true }
before do
stub_feature_flags(gitpod: gitpod_feature_enabled)
visit general_admin_application_settings_path
end
@ -205,6 +208,32 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
expect(page).to have_content "Application settings saved successfully"
expect(current_settings.terminal_max_session_time).to eq(15)
end
context 'Configure Gitpod' do
context 'with feature disabled' do
let(:gitpod_feature_enabled) { false }
it 'do not show settings' do
expect(page).not_to have_selector('#js-gitpod-settings')
end
end
context 'with feature enabled' do
let(:gitpod_feature_enabled) { true }
it 'changes gitpod settings' do
page.within('#js-gitpod-settings') do
check 'Enable Gitpod integration'
fill_in 'Gitpod URL', with: 'https://gitpod.test/'
click_button 'Save changes'
end
expect(page).to have_content 'Application settings saved successfully'
expect(current_settings.gitpod_url).to eq('https://gitpod.test/')
expect(current_settings.gitpod_enabled).to be(true)
end
end
end
end
context 'Integrations page' do

View File

@ -68,7 +68,7 @@ exports[`Design note component should match the snapshot 1`] = `
</div>
<div
class="note-text js-note-text"
class="note-text js-note-text md"
data-qa-selector="note_content"
/>

View File

@ -1,51 +0,0 @@
import { mount } from '@vue/test-utils';
import WebIdeLink from '~/repository/components/web_ide_link.vue';
describe('Web IDE link component', () => {
let wrapper;
function createComponent(props) {
wrapper = mount(WebIdeLink, {
propsData: { ...props },
mocks: {
$route: {
params: {},
},
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders link to the Web IDE for a project if only projectPath is given', () => {
createComponent({ projectPath: 'gitlab-org/gitlab', refSha: 'master' });
expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
expect(wrapper.text()).toBe('Web IDE');
});
it('renders link to the Web IDE for a project even if both projectPath and forkPath are given', () => {
createComponent({
projectPath: 'gitlab-org/gitlab',
refSha: 'master',
forkPath: 'my-namespace/gitlab',
});
expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
expect(wrapper.text()).toBe('Web IDE');
});
it('renders link to the forked project if it exists and cannot write to the repo', () => {
createComponent({
projectPath: 'gitlab-org/gitlab',
refSha: 'master',
forkPath: 'my-namespace/gitlab',
canPushCode: false,
});
expect(wrapper.attributes('href')).toBe('/-/ide/project/my-namespace/gitlab/edit/master/-/');
expect(wrapper.text()).toBe('Edit fork in Web IDE');
});
});

View File

@ -0,0 +1,203 @@
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlLink } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_ACTION = {
key: 'action1',
text: 'Sample',
secondaryText: 'Lorem ipsum.',
tooltip: '',
href: '/sample',
attrs: { 'data-test': '123' },
};
const TEST_ACTION_2 = {
key: 'action2',
text: 'Sample 2',
secondaryText: 'Dolar sit amit.',
tooltip: 'Dolar sit amit.',
href: '#',
attrs: { 'data-test': '456' },
};
const TEST_TOOLTIP = 'Lorem ipsum dolar sit';
describe('Actions button component', () => {
let wrapper;
function createComponent(props) {
wrapper = shallowMount(ActionsButton, {
propsData: { ...props },
directives: { GlTooltip: createMockDirective() },
});
}
afterEach(() => {
wrapper.destroy();
});
const getTooltip = child => {
const directiveBinding = getBinding(child.element, 'gl-tooltip');
return directiveBinding.value;
};
const findLink = () => wrapper.find(GlLink);
const findLinkTooltip = () => getTooltip(findLink());
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownTooltip = () => getTooltip(findDropdown());
const parseDropdownItems = () =>
findDropdown()
.findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
.wrappers.map(x => {
if (x.is('gl-dropdown-divider-stub')) {
return { type: 'divider' };
}
const { isCheckItem, isChecked, secondaryText } = x.props();
return {
type: 'item',
isCheckItem,
isChecked,
secondaryText,
text: x.text(),
};
});
const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt);
const clickLink = (...args) => clickOn(findLink(), ...args);
const clickDropdown = (...args) => clickOn(findDropdown(), ...args);
describe('with 1 action', () => {
beforeEach(() => {
createComponent({ actions: [TEST_ACTION] });
});
it('should not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
it('should render single button', () => {
const link = findLink();
expect(link.attributes()).toEqual({
class: expect.any(String),
href: TEST_ACTION.href,
...TEST_ACTION.attrs,
});
expect(link.text()).toBe(TEST_ACTION.text);
});
it('should have tooltip', () => {
expect(findLinkTooltip()).toBe(TEST_ACTION.tooltip);
});
it('should have attrs', () => {
expect(findLink().attributes()).toMatchObject(TEST_ACTION.attrs);
});
it('can click', () => {
expect(clickLink).not.toThrow();
});
});
describe('with 1 action with tooltip', () => {
it('should have tooltip', () => {
createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
expect(findLinkTooltip()).toBe(TEST_TOOLTIP);
});
});
describe('with 1 action with handle', () => {
it('can click and trigger handle', () => {
const handleClick = jest.fn();
createComponent({ actions: [{ ...TEST_ACTION, handle: handleClick }] });
const event = new Event('click');
clickLink(event);
expect(handleClick).toHaveBeenCalledWith(event);
});
});
describe('with multiple actions', () => {
let handleAction;
beforeEach(() => {
handleAction = jest.fn();
createComponent({ actions: [{ ...TEST_ACTION, handle: handleAction }, TEST_ACTION_2] });
});
it('should default to selecting first action', () => {
expect(findDropdown().attributes()).toMatchObject({
text: TEST_ACTION.text,
'split-href': TEST_ACTION.href,
});
});
it('should handle first action click', () => {
const event = new Event('click');
clickDropdown(event);
expect(handleAction).toHaveBeenCalledWith(event);
});
it('should render dropdown items', () => {
expect(parseDropdownItems()).toEqual([
{
type: 'item',
isCheckItem: true,
isChecked: true,
secondaryText: TEST_ACTION.secondaryText,
text: TEST_ACTION.text,
},
{ type: 'divider' },
{
type: 'item',
isCheckItem: true,
isChecked: false,
secondaryText: TEST_ACTION_2.secondaryText,
text: TEST_ACTION_2.text,
},
]);
});
it('should select action 2 when clicked', () => {
expect(wrapper.emitted('select')).toBeUndefined();
const action2 = wrapper.find(`[data-testid="action_${TEST_ACTION_2.key}"]`);
action2.vm.$emit('click');
expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
});
it('should have tooltip value', () => {
expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
});
});
describe('with multiple actions and selectedKey', () => {
beforeEach(() => {
createComponent({ actions: [TEST_ACTION, TEST_ACTION_2], selectedKey: TEST_ACTION_2.key });
});
it('should show action 2 as selected', () => {
expect(parseDropdownItems()).toEqual([
expect.objectContaining({
type: 'item',
isChecked: false,
}),
{ type: 'divider' },
expect.objectContaining({
type: 'item',
isChecked: true,
}),
]);
});
it('should have tooltip value', () => {
expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
});
});
});

View File

@ -0,0 +1,106 @@
import { shallowMount } from '@vue/test-utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const ACTION_WEB_IDE = {
href: TEST_WEB_IDE_URL,
key: 'webide',
secondaryText: 'Quickly and easily edit multiple files in your project.',
tooltip: '',
text: 'Web IDE',
attrs: {
'data-qa-selector': 'web_ide_button',
},
};
const ACTION_WEB_IDE_FORK = {
...ACTION_WEB_IDE,
href: '#modal-confirm-fork',
handle: expect.any(Function),
};
const ACTION_GITPOD = {
href: TEST_GITPOD_URL,
key: 'gitpod',
secondaryText: 'Launch a ready-to-code development environment for your project.',
tooltip: 'Launch a ready-to-code development environment for your project.',
text: 'Gitpod',
attrs: {
'data-qa-selector': 'gitpod_button',
},
};
const ACTION_GITPOD_ENABLE = {
...ACTION_GITPOD,
href: '#modal-enable-gitpod',
handle: expect.any(Function),
};
describe('Web IDE link component', () => {
let wrapper;
function createComponent(props) {
wrapper = shallowMount(WebIdeLink, {
propsData: {
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
it.each`
props | expectedActions
${{}} | ${[ACTION_WEB_IDE]}
${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]}
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]}
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]}
${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]}
`('renders actions with props=$props', ({ props, expectedActions }) => {
createComponent(props);
expect(findActionsButton().props('actions')).toEqual(expectedActions);
});
describe('with multiple actions', () => {
beforeEach(() => {
createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true });
});
it('selected Web IDE by default', () => {
expect(findActionsButton().props()).toMatchObject({
actions: [ACTION_WEB_IDE, ACTION_GITPOD],
selectedKey: ACTION_WEB_IDE.key,
});
});
it('should set selection with local storage value', async () => {
expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key);
findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
await wrapper.vm.$nextTick();
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
});
it('should update local storage when selection changes', async () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
await wrapper.vm.$nextTick();
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
});
});
});

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