Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-27 18:08:59 +00:00
parent 2b1e7f7dac
commit f3b9e205bb
39 changed files with 690 additions and 571 deletions

View File

@ -15,7 +15,7 @@ code_quality:
stage: test stage: test
needs: [] needs: []
variables: variables:
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1" CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18"
script: script:
- | - |
if ! docker info &>/dev/null; then if ! docker info &>/dev/null; then

View File

@ -14,6 +14,7 @@ tasks:
set -e set -e
cd /workspace/gitlab-development-kit cd /workspace/gitlab-development-kit
[[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab [[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab
mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config
# make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace # make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace
echo "webpack:" >> gdk.yml echo "webpack:" >> gdk.yml
echo " static: true" >> gdk.yml echo " static: true" >> gdk.yml
@ -48,14 +49,6 @@ tasks:
if [ "$GITLAB_RUN_DB_MIGRATIONS" == true ]; then if [ "$GITLAB_RUN_DB_MIGRATIONS" == true ]; then
make gitlab-db-migrate make gitlab-db-migrate
fi fi
# Fix DB key
if [ "$GITLAB_FIX_DB_KEY" = true ]; then
echo "$(date) Fixing DB key" | tee -a /workspace/startup.log
cd gitlab
# see https://gitlab.com/gitlab-org/gitlab-foss/-/issues/56403#note_132515069
printf 'ApplicationSetting.last.update_column(:runners_registration_token_encrypted, nil)\nexit\n' | bundle exec rails c
cd -
fi
# Waiting for GitLab ... # Waiting for GitLab ...
gp await-port 3000 gp await-port 3000
printf "Waiting for GitLab at $(gp url 3000) ..." printf "Waiting for GitLab at $(gp url 3000) ..."

View File

@ -5,11 +5,11 @@ import { uniq } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __ } from './locale'; import { __ } from './locale';
import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { deprecatedCreateFlash as flash } from './flash'; import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import * as Emoji from '~/emoji'; import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@ -374,7 +374,7 @@ export class AwardsHandler {
counter.text(counterNumber - 1); counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton); this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('dispose'); dispose($emojiButton);
counter.text('0'); counter.text('0');
this.removeYouFromUserList($emojiButton); this.removeYouFromUserList($emojiButton);
if ($emojiButton.parents('.note').length) { if ($emojiButton.parents('.note').length) {
@ -387,7 +387,8 @@ export class AwardsHandler {
} }
removeEmoji($emojiButton) { removeEmoji($emojiButton) {
$emojiButton.tooltip('dispose'); dispose($emojiButton);
$emojiButton.remove(); $emojiButton.remove();
const $votesBlock = this.getVotesBlock(); const $votesBlock = this.getVotesBlock();
if ($votesBlock.find('.js-emoji-btn').length === 0) { if ($votesBlock.find('.js-emoji-btn').length === 0) {
@ -415,13 +416,17 @@ export class AwardsHandler {
const originalTitle = this.getAwardTooltip(awardBlock); const originalTitle = this.getAwardTooltip(awardBlock);
const authors = originalTitle.split(FROM_SENTENCE_REGEX); const authors = originalTitle.split(FROM_SENTENCE_REGEX);
authors.splice(authors.indexOf('You'), 1); authors.splice(authors.indexOf('You'), 1);
return awardBlock
awardBlock
.closest('.js-emoji-btn') .closest('.js-emoji-btn')
.removeData('title') .removeData('title')
.removeAttr('data-title') .removeAttr('data-title')
.removeAttr('data-original-title') .removeAttr('data-original-title')
.attr('title', this.toSentence(authors)) .attr('title', this.toSentence(authors));
.tooltip('_fixTitle');
fixTitle(awardBlock);
return awardBlock;
} }
addYouToUserList(votesBlock, emoji) { addYouToUserList(votesBlock, emoji) {
@ -432,7 +437,12 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.unshift('You'); users.unshift('You');
return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
awardBlock.attr('title', this.toSentence(users));
fixTitle(awardBlock);
return awardBlock;
} }
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
@ -448,7 +458,7 @@ export class AwardsHandler {
.find('.emoji-icon') .find('.emoji-icon')
.data('name', emojiName); .data('name', emojiName);
this.animateEmoji($emojiButton); this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current'); votesBlock.removeClass('current');
} }
@ -487,17 +497,6 @@ export class AwardsHandler {
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
} }
userAuthored($emojiButton) {
const oldTitle = this.getAwardTooltip($emojiButton);
const newTitle = 'You cannot vote on your own issue, MR and note';
updateTooltipTitle($emojiButton, newTitle).tooltip('show');
// Restore tooltip back to award list
return setTimeout(() => {
$emojiButton.tooltip('hide');
updateTooltipTitle($emojiButton, oldTitle);
}, 2800);
}
scrollToAwards() { scrollToAwards() {
const options = { const options = {
scrollTop: $('.awards').offset().top - 110, scrollTop: $('.awards').offset().top - 110,

View File

@ -61,9 +61,6 @@ export const rstrip = val => {
return val; return val;
}; };
export const updateTooltipTitle = ($tooltipEl, newTitle) =>
$tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector); const field = $(fieldSelector);
const closestSubmit = field.closest('form').find(buttonSelector); const closestSubmit = field.closest('form').find(buttonSelector);

View File

@ -6,6 +6,13 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
layout 'fullscreen' layout 'fullscreen'
content_security_policy do |policy|
next if policy.directives.blank?
frame_src_values = Array.wrap(policy.directives['frame-src']) | ['https://www.youtube.com']
policy.frame_src(*frame_src_values)
end
prepend_before_action :authenticate_user!, only: [:show] prepend_before_action :authenticate_user!, only: [:show]
before_action :assign_ref_and_path, only: [:show] before_action :assign_ref_and_path, only: [:show]
before_action :authorize_edit_tree!, only: [:show] before_action :authorize_edit_tree!, only: [:show]

View File

@ -377,7 +377,12 @@ module IssuablesHelper
end end
def issuable_display_type(issuable) def issuable_display_type(issuable)
issuable.model_name.human.downcase case issuable
when Issue
issuable.issue_type.downcase
when MergeRequest
issuable.model_name.human.downcase
end
end end
def has_filter_bar_param? def has_filter_bar_param?

View File

@ -293,9 +293,7 @@ class Snippet < ApplicationRecord
@storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX) @storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX)
end end
# This is the full_path used to identify the # This is the full_path used to identify the the snippet repository.
# the snippet repository. It will be used mostly
# for logging purposes.
override :full_path override :full_path
def full_path def full_path
return unless persisted? return unless persisted?
@ -303,7 +301,7 @@ class Snippet < ApplicationRecord
@full_path ||= begin @full_path ||= begin
components = [] components = []
components << project.full_path if project_id? components << project.full_path if project_id?
components << '@snippets' components << 'snippets'
components << self.id components << self.id
components.join('/') components.join('/')
end end

View File

@ -1 +1,6 @@
= render template: 'projects/issues/show' - @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents")
= render 'projects/issuable/show', issuable: @issue

View File

@ -0,0 +1,10 @@
- page_description issuable.description_html
- page_card_attributes issuable.card_attributes
- if issuable.relocation_target
- page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
= render_if_exists "projects/issues/alert_blocked", issue: issuable, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_header', issuable: issuable
= render 'shared/issue_type/details_content', issuable: issuable

View File

@ -1,103 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Issues"), project_issues_path(@project) - add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference - breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") - page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- page_description @issue.description_html
- page_card_attributes @issue.card_attributes
- if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
- if @issue.sentry_issue.present?
- add_page_specific_style 'page_bundles/error_tracking_details'
- can_update_issue = can?(current_user, :update_issue, @issue) = render 'projects/issuable/show', issuable: @issue
- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
- related_branches_path = related_branches_project_issue_path(@project, @issue)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: @issue
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) }
= sprite_icon('mobile-issue-close', css_class: 'd-block d-sm-none')
.d-none.d-sm-block
= issue_closed_text(@issue, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(@issue, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block Open
.issuable-meta
#js-issuable-header-warnings
= issuable_meta(@issue, @project, "Issue")
%a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.float-left.d-md-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none
%ul
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project), class: 'd-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
.detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(@issue).to_json} }
.title-container
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
.description
.md= markdown_field(@issue, :description)
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
= render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
- add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issue: @issue
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees

View File

@ -0,0 +1,31 @@
- related_branches_path = related_branches_project_issue_path(@project, issuable)
.issue-details.issuable-details
.detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json} }
.title-container
%h2.title= markdown_field(issuable, :title)
- if issuable.description.present?
.description
.md= markdown_field(issuable, :description)
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
= render 'shared/issue_type/sentry_stack_trace', issuable: issuable
= render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
- add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
= render 'shared/issue_type/emoji_block', issuable: issuable
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees

View File

@ -0,0 +1,52 @@
- can_update_issue = can?(current_user, :update_issue, issuable)
- can_reopen_issue = can?(current_user, :reopen_issue, issuable)
- can_report_spam = issuable.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
- display_issuable_type = issuable_display_type(issuable)
- new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
= sprite_icon('mobile-issue-close', css_class: 'gl-display-block gl-display-sm-none!')
.gl-display-none.gl-display-sm-block!
= issue_closed_text(issuable, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'gl-display-block gl-display-sm-none!')
%span.gl-display-none.gl-display-sm-block!
= _('Open')
.issuable-meta
#js-issuable-header-warnings
= issuable_meta(issuable, @project, display_issuable_type)
%a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
%button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
= _('Options')
= icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- unless current_user == issuable.author
%li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable))
- if can_update_issue
%li= link_to _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(issuable, true)}", title: _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
- if can_reopen_issue
%li= link_to _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(issuable, false)}", title: _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
- if can_report_spam
%li= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, new_project_issue_path(@project, new_issuable_params), id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type }
= render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked?
- if can_report_spam
= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do
= _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }

View File

@ -0,0 +1,9 @@
.content-block.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: issuable, inline: true
.new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issuable: issuable
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(issuable), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?

View File

@ -0,0 +1,4 @@
- return unless issuable.sentry_issue.present?
- add_page_specific_style 'page_bundles/error_tracking_details'
#js-sentry-error-stack-trace{ data: error_details_data(@project, issuable.sentry_issue.sentry_issue_identifier) }

View File

@ -0,0 +1,5 @@
---
title: Configure CSP for displaying Youtube videos in the Static Site Editor
merge_request: 45767
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Use CodeQuality 0.85.18 in the CI template
merge_request: 46253
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add environment variables to override backup/restore DB settings
merge_request: 45855
author:
type: added

View File

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

View File

@ -3,7 +3,7 @@ comments: false
description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.' description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
--- ---
<div class="display-none"> <div class="d-none">
<em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized <em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized
navigation, discoverability, and readability.</em> navigation, discoverability, and readability.</em>
</div> </div>

View File

@ -96,7 +96,7 @@ POST /projects/:id/protected_environments
``` ```
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_environments?name=staging&deploy_access_levels%5B%5D%5Buser_id%5D=1" curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments"
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
@ -105,21 +105,22 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
| `name` | string | yes | The name of the environment. | | `name` | string | yes | The name of the environment. |
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. | | `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. |
Elements in the `deploy_access_levels` array should take the Elements in the `deploy_access_levels` array should be one of `user_id`, `group_id` or
form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. `access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or
`{access_level: integer}`.
Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md).
Example response: Example response:
```json ```json
{ {
"name":"staging", "name":"production",
"deploy_access_levels":[ "deploy_access_levels":[
{ {
"access_level":null, "access_level":40,
"access_level_description":"Administrator", "access_level_description":"protected-access-group",
"user_id":1, "user_id":null,
"group_id":null "group_id":9899826
} }
] ]
} }

View File

@ -940,9 +940,7 @@ message. Install the [correct GitLab version](https://packages.gitlab.com/gitlab
and then try again. and then try again.
NOTE: **Note:** NOTE: **Note:**
There is a known issue with restore not working with `pgbouncer`. The [workaround is to bypass There is a known issue with restore not working with `pgbouncer`. [Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer).
`pgbouncer` and connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer).
[Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer).
### Restore for Docker image and GitLab Helm chart installations ### Restore for Docker image and GitLab Helm chart installations
@ -1039,26 +1037,60 @@ practical use.
## Backup and restore for installations using PgBouncer ## Backup and restore for installations using PgBouncer
PgBouncer can cause the following errors when performing backups and restores: Do NOT backup or restore GitLab through a PgBouncer connection. These
tasks must [bypass PgBouncer and connect directly to the PostgreSQL primary database node](#bypassing-pgbouncer),
or they will cause a GitLab outage.
When the GitLab backup or restore task is used with PgBouncer, the
following error message is shown:
```ruby ```ruby
ActiveRecord::StatementInvalid: PG::UndefinedTable ActiveRecord::StatementInvalid: PG::UndefinedTable
``` ```
There is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3470) for restore not working This happens because the task uses `pg_dump`, which [sets a null search
with `pgbouncer`. path and explicitly includes the schema in every SQL query](https://gitlab.com/gitlab-org/gitlab/-/issues/23211)
to address [CVE-2018-1058](https://www.postgresql.org/about/news/postgresql-103-968-9512-9417-and-9322-released-1834/).
To workaround this issue, the GitLab server will need to bypass `pgbouncer` and Since connections are reused with PgBouncer in transaction pooling mode,
[connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer) PostgreSQL fails to search the default `public` schema. As a result,
to perform the database restore. this clearing of the search path causes tables and columns to appear
missing.
There is also a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/23211) ### Bypassing PgBouncer
with PostgreSQL 9 and running a database backup through PgBouncer that can cause
an outage to GitLab. If you're still on PostgreSQL 9 and upgrading PostgreSQL isn't There are two ways to fix this:
an option, workarounds include having a dedicated application node just for backups,
configured to connect directly the primary database node as noted above. You're 1. [Use environment variables to override the database settings](#environment-variable-overrides) for the backup task.
advised to upgrade your PostgreSQL version though, GitLab 11.11 shipped with PostgreSQL 1. Reconfigure a node to [connect directly to the PostgreSQL primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer).
10.7, and that is the recommended version for GitLab 12+.
#### Environment variable overrides
By default, GitLab uses the database configuration stored in a
configuration file (`database.yml`). However, you can override the database settings
for the backup and restore task by setting environment
variables that are prefixed with `GITLAB_BACKUP_`:
- `GITLAB_BACKUP_PGHOST`
- `GITLAB_BACKUP_PGUSER`
- `GITLAB_BACKUP_PGPORT`
- `GITLAB_BACKUP_PGPASSWORD`
- `GITLAB_BACKUP_PGSSLMODE`
- `GITLAB_BACKUP_PGSSLKEY`
- `GITLAB_BACKUP_PGSSLCERT`
- `GITLAB_BACKUP_PGSSLROOTCERT`
- `GITLAB_BACKUP_PGSSLCRL`
- `GITLAB_BACKUP_PGSSLCOMPRESSION`
For example, to override the database host and port to use 192.168.1.10
and port 5432 with the Omnibus package:
```shell
sudo GITLAB_BACKUP_PGHOST=192.168.1.10 GITLAB_BACKUP_PGPORT=5432 /opt/gitlab/bin/gitlab-backup create
```
See the [PostgreSQL documentation](https://www.postgresql.org/docs/12/libpq-envars.html)
for more details on what these parameters do.
## Additional notes ## Additional notes

View File

@ -30,6 +30,7 @@ Docker image with the fuzz engine to run your app.
| Swift | [libfuzzer](https://github.com/apple/swift/blob/master/docs/libFuzzerIntegration.md) | [swift-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/swift-fuzzing-example) | | Swift | [libfuzzer](https://github.com/apple/swift/blob/master/docs/libFuzzerIntegration.md) | [swift-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/swift-fuzzing-example) |
| Rust | [cargo-fuzz (libFuzzer support)](https://github.com/rust-fuzz/cargo-fuzz) | [rust-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/rust-fuzzing-example) | | Rust | [cargo-fuzz (libFuzzer support)](https://github.com/rust-fuzz/cargo-fuzz) | [rust-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/rust-fuzzing-example) |
| Java | [JQF](https://github.com/rohanpadhye/JQF) | [java-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) | | Java | [JQF](https://github.com/rohanpadhye/JQF) | [java-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) |
| Java | [javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) |
## Configuration ## Configuration

View File

@ -30,31 +30,33 @@ The Package Registry supports the following formats:
You can also use the [API](../../api/packages.md) to administer the Package Registry. You can also use the [API](../../api/packages.md) to administer the Package Registry.
The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. ## Accepting contributions
It's built on open source software and completely integrated within GitLab.
Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to
manage the registry across groups and projects.
The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages. The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages.md) will
## Suggested contributions
Consider contributing to GitLab. This [development documentation](../../development/packages.md) will
guide you through the process. Or check out how other members of the community guide you through the process. Or check out how other members of the community
are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17417) or [Terraform](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834). are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17417) or [Terraform](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834).
| Format | Use case | | Format | Status |
| ------ | ------ | | ------ | ------ |
| [Cargo](https://gitlab.com/gitlab-org/gitlab/-/issues/33060) | Cargo is the Rust package manager. Build, publish and share Rust packages | | Chef | [#36889](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) |
| [Chef](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) | Configuration management with Chef using all the benefits of a repository manager. | | CocoaPods | [#36890](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) |
| [CocoaPods](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) | Speed up development with Xcode and CocoaPods. | | CocoaPods | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) |
| [Conda](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | Secure and private local Conda repositories. | | Conda | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) |
| [CRAN](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | Deploy and resolve CRAN packages for the R language. | | CRAN | [#36892](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) |
| [Debian](https://gitlab.com/gitlab-org/gitlab/-/issues/5835) | Host and provision Debian packages. | | Debian | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44746) |
| [Opkg](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | Optimize your work with OpenWrt using Opkg repositories. | | Opkg | [#36894](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) |
| [P2](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | Host all your Eclipse plugins in your own GitLab P2 repository. | | P2 | [#36895](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) |
| [Puppet](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | Configuration management meets repository management with Puppet repositories. | | Puppet | [#36897](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) |
| [RPM](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) | Distribute RPMs directly from GitLab. | | RPM | [#5932](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) |
| [RubyGems](https://gitlab.com/gitlab-org/gitlab/-/issues/803) | Use GitLab to host your own gems. | | RubyGems | [#803](https://gitlab.com/gitlab-org/gitlab/-/issues/803) |
| [SBT](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) | Resolve dependencies from and deploy build output to SBT repositories when running SBT builds. | | SBT | [#36898](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) |
| [Vagrant](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) | Securely host your Vagrant boxes in local repositories. | | Terraform | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834) |
| Vagrant | [#36899](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) |
## Container Registry
The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. It's built on open source software and completely integrated within GitLab. Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to manage the registry across groups and projects.
## Dependency Proxy
The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages.

View File

@ -140,7 +140,14 @@ module Backup
'sslcrl' => 'PGSSLCRL', 'sslcrl' => 'PGSSLCRL',
'sslcompression' => 'PGSSLCOMPRESSION' 'sslcompression' => 'PGSSLCOMPRESSION'
} }
args.each { |opt, arg| ENV[arg] = config[opt].to_s if config[opt] } args.each do |opt, arg|
# This enables the use of different PostgreSQL settings in
# case PgBouncer is used. PgBouncer clears the search path,
# which wreaks havoc on Rails if connections are reused.
override = "GITLAB_BACKUP_#{arg}"
val = ENV[override].presence || config[opt].to_s.presence
ENV[arg] = val if val
end
end end
def report_success(success) def report_success(success)

View File

@ -7,7 +7,7 @@ code_quality:
variables: variables:
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "" DOCKER_TLS_CERTDIR: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1" CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18"
needs: [] needs: []
script: script:
- export SOURCE_CODE=$PWD - export SOURCE_CODE=$PWD

View File

@ -67,15 +67,6 @@ module Gitlab
::JSON.pretty_generate(object, opts) ::JSON.pretty_generate(object, opts)
end end
# Feature detection for using Oj instead of the `json` gem.
#
# @return [Boolean]
def enable_oj?
return false unless feature_table_exists?
Feature.enabled?(:oj_json, default_enabled: true)
end
private private
# Convert JSON string into Ruby through toggleable adapters. # Convert JSON string into Ruby through toggleable adapters.
@ -91,11 +82,7 @@ module Gitlab
def adapter_load(string, *args, **opts) def adapter_load(string, *args, **opts)
opts = standardize_opts(opts) opts = standardize_opts(opts)
if enable_oj? Oj.load(string, opts)
Oj.load(string, opts)
else
::JSON.parse(string, opts)
end
rescue Oj::ParseError, Encoding::UndefinedConversionError => ex rescue Oj::ParseError, Encoding::UndefinedConversionError => ex
raise parser_error.new(ex) raise parser_error.new(ex)
end end
@ -120,11 +107,7 @@ module Gitlab
# #
# @return [String] # @return [String]
def adapter_dump(object, *args, **opts) def adapter_dump(object, *args, **opts)
if enable_oj? Oj.dump(object, opts)
Oj.dump(object, opts)
else
::JSON.dump(object, *args)
end
end end
# Generates JSON for an object but with fewer options, using toggleable adapters. # Generates JSON for an object but with fewer options, using toggleable adapters.
@ -135,11 +118,7 @@ module Gitlab
def adapter_generate(object, opts = {}) def adapter_generate(object, opts = {})
opts = standardize_opts(opts) opts = standardize_opts(opts)
if enable_oj? Oj.generate(object, opts)
Oj.generate(object, opts)
else
::JSON.generate(object, opts)
end
end end
# Take a JSON standard options hash and standardize it to work across adapters # Take a JSON standard options hash and standardize it to work across adapters
@ -149,11 +128,8 @@ module Gitlab
# @return [Hash] # @return [Hash]
def standardize_opts(opts) def standardize_opts(opts)
opts ||= {} opts ||= {}
opts[:mode] = :rails
if enable_oj? opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
opts[:mode] = :rails
opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
end
opts opts
end end
@ -213,7 +189,7 @@ module Gitlab
# @param object [Object] # @param object [Object]
# @return [String] # @return [String]
def self.call(object, env = nil) def self.call(object, env = nil)
if Gitlab::Json.enable_oj? && Feature.enabled?(:grape_gitlab_json, default_enabled: true) if Feature.enabled?(:grape_gitlab_json, default_enabled: true)
Gitlab::Json.dump(object) Gitlab::Json.dump(object)
else else
Grape::Formatter::Json.call(object, env) Grape::Formatter::Json.call(object, env)

View File

@ -32,7 +32,7 @@ module Gitlab
def changes_will_exceed_size_limit?(change_size) def changes_will_exceed_size_limit?(change_size)
return false unless enabled? return false unless enabled?
change_size > limit || exceeded_size(change_size) > 0 above_size_limit? || exceeded_size(change_size) > 0
end end
# @param change_size [int] in bytes # @param change_size [int] in bytes

View File

@ -4,9 +4,6 @@ module Gitlab
module RepositoryUrlBuilder module RepositoryUrlBuilder
class << self class << self
def build(path, protocol: :ssh) def build(path, protocol: :ssh)
# TODO: See https://gitlab.com/gitlab-org/gitlab/-/issues/213021
path = path.sub('@snippets', 'snippets')
case protocol case protocol
when :ssh when :ssh
ssh_url(path) ssh_url(path)

View File

@ -17716,6 +17716,9 @@ msgstr ""
msgid "New" msgid "New"
msgstr "" msgstr ""
msgid "New %{display_issuable_type}"
msgstr ""
msgid "New Application" msgid "New Application"
msgstr "" msgstr ""
@ -25457,6 +25460,9 @@ msgstr ""
msgid "Submit a review" msgid "Submit a review"
msgstr "" msgstr ""
msgid "Submit as spam"
msgstr ""
msgid "Submit changes" msgid "Submit changes"
msgstr "" msgstr ""

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "User views incident" do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) }
let_it_be(:note) { create(:note, noteable: incident, project: project, author: user) }
before_all do
project.add_developer(user)
end
before do
sign_in(user)
visit(project_issues_incident_path(project, incident))
end
it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
it 'shows the merge request and incident actions', :aggregate_failures do
expect(page).to have_link('New incident')
expect(page).to have_button('Create merge request')
expect(page).to have_link('Close incident')
end
context 'when the project is archived' do
before do
project.update!(archived: true)
visit(project_issues_incident_path(project, incident))
end
it 'hides the merge request and incident actions', :aggregate_failures do
expect(page).not_to have_link('New incident')
expect(page).not_to have_button('Create merge request')
expect(page).not_to have_link('Close incident')
end
end
describe 'user status' do
subject { visit(project_issues_incident_path(project, incident)) }
context 'when showing status of the author of the incident' do
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
end
end
context 'when showing status of a user who commented on an incident', :js do
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
end
end
context 'when status message has an emoji', :js do
let_it_be(:message) { 'My status with an emoji' }
let_it_be(:message_emoji) { 'basketball' }
let_it_be(:status) { create(:user_status, user: user, emoji: 'smirk', message: "#{message} :#{message_emoji}:") }
it 'correctly renders the emoji' do
wait_for_requests
tooltip_span = page.first(".user-status-emoji[title^='#{message}']")
tooltip_span.hover
wait_for_requests
tooltip = page.find('.tooltip .tooltip-inner')
page.within(tooltip) do
expect(page).to have_emoji(message_emoji)
end
end
end
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe 'User interacts with awards' do
page.within('.awards') do page.within('.awards') do
expect(page).to have_selector('.js-emoji-btn') expect(page).to have_selector('.js-emoji-btn')
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") expect(page).to have_css(".js-emoji-btn.active[title='You']")
expect do expect do
page.find('.js-emoji-btn.active').click page.find('.js-emoji-btn.active').click

View File

@ -34,7 +34,7 @@ RSpec.describe 'User uses header search field', :js do
wait_for_all_requests wait_for_all_requests
end end
it 'shows the category search dropdown' do it 'shows the category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
end end
end end
@ -44,7 +44,7 @@ RSpec.describe 'User uses header search field', :js do
page.find('#search').click page.find('#search').click
end end
it 'shows category search dropdown' do it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
end end
@ -104,7 +104,7 @@ RSpec.describe 'User uses header search field', :js do
let(:scope_name) { 'All GitLab' } let(:scope_name) { 'All GitLab' }
end end
it 'displays search options' do it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do
fill_in_search('test') fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test'))

View File

@ -73,4 +73,44 @@ RSpec.describe 'Static Site Editor' do
expect(node['data-static-site-generator']).to eq('middleman') expect(node['data-static-site-generator']).to eq('middleman')
end end
end end
describe 'Static Site Editor Content Security Policy' do
subject { response_headers['Content-Security-Policy'] }
context 'when no global CSP config exists' do
before do
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
end
it 'does not add CSP directives' do
visit sse_path
is_expected.to be_blank
end
end
context 'when a global CSP config exists' do
let_it_be(:cdn_url) { 'https://some-cdn.test' }
let_it_be(:youtube_url) { 'https://www.youtube.com' }
before do
csp = ActionDispatch::ContentSecurityPolicy.new do |p|
p.frame_src :self, cdn_url
end
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
end
it 'appends youtube to the CSP frame-src policy' do
visit sse_path
is_expected.to eql("frame-src 'self' #{cdn_url} #{youtube_url}")
end
end
end
end end

View File

@ -169,29 +169,6 @@ describe('AwardsHandler', () => {
}); });
}); });
describe('::userAuthored', () => {
it('should update tooltip to user authored title', () => {
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
expect($thumbsUpEmoji.data('originalTitle')).toBe(
'You cannot vote on your own issue, MR and note',
);
});
it('should restore tooltip back to initial vote list', () => {
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
jest.advanceTimersByTime(2801);
expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::getAwardUrl', () => { describe('::getAwardUrl', () => {
it('returns the url for request', () => { it('returns the url for request', () => {
expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji'); expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji');

View File

@ -345,6 +345,24 @@ RSpec.describe IssuablesHelper do
end end
end end
describe '#issuable_display_type' do
using RSpec::Parameterized::TableSyntax
where(:issuable_type, :issuable_display_type) do
:issue | 'issue'
:incident | 'incident'
:merge_request | 'merge request'
end
with_them do
let(:issuable) { build_stubbed(issuable_type) }
subject { helper.issuable_display_type(issuable) }
it { is_expected.to eq(issuable_display_type) }
end
end
describe '#sidebar_milestone_tooltip_label' do describe '#sidebar_milestone_tooltip_label' do
it 'escapes HTML in the milestone title' do it 'escapes HTML in the milestone title' do
milestone = build(:milestone, title: '&lt;img onerror=alert(1)&gt;') milestone = build(:milestone, title: '&lt;img onerror=alert(1)&gt;')

View File

@ -48,5 +48,26 @@ RSpec.describe Backup::Database do
expect(output).to include(visible_error) expect(output).to include(visible_error)
end end
end end
context 'with PostgreSQL settings defined in the environment' do
let(:cmd) { %W[#{Gem.ruby} -e] + ["$stderr.puts ENV.to_h.select { |k, _| k.start_with?('PG') }"] }
let(:config) { YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))['test'] }
before do
stub_const 'ENV', ENV.to_h.merge({
'GITLAB_BACKUP_PGHOST' => 'test.example.com',
'PGPASSWORD' => 'donotchange'
})
end
it 'overrides default config values' do
subject.restore
expect(output).to include(%("PGHOST"=>"test.example.com"))
expect(output).to include(%("PGPASSWORD"=>"donotchange"))
expect(output).to include(%("PGPORT"=>"#{config['port']}")) if config['port']
expect(output).to include(%("PGUSER"=>"#{config['username']}")) if config['username']
end
end
end end
end end

View File

@ -7,342 +7,306 @@ RSpec.describe Gitlab::Json do
stub_feature_flags(json_wrapper_legacy_mode: true) stub_feature_flags(json_wrapper_legacy_mode: true)
end end
shared_examples "json" do describe ".parse" do
describe ".parse" do context "legacy_mode is disabled by default" do
context "legacy_mode is disabled by default" do it "parses an object" do
it "parses an object" do expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: false)).to be(false)
end
end end
context "legacy_mode is enabled" do it "parses an array" do
it "parses an object" do expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "raises an error on a string" do
expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a true bool" do
expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a false bool" do
expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
end end
context "feature flag is disabled" do it "parses a string" do
before do expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
stub_feature_flags(json_wrapper_legacy_mode: false) end
end
it "parses an object" do it "parses a true bool" do
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) expect(subject.parse("true", legacy_mode: false)).to be(true)
end end
it "parses an array" do it "parses a false bool" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) expect(subject.parse("false", legacy_mode: false)).to be(false)
end
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: true)).to be(false)
end
end end
end end
describe ".parse!" do context "legacy_mode is enabled" do
context "legacy_mode is disabled by default" do it "parses an object" do
it "parses an object" do expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: false)).to be(false)
end
end end
context "legacy_mode is enabled" do it "parses an array" do
it "parses an object" do expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "raises an error on a string" do
expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a true bool" do
expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a false bool" do
expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
end end
context "feature flag is disabled" do it "raises an error on a string" do
before do expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
stub_feature_flags(json_wrapper_legacy_mode: false) end
end
it "parses an object" do it "raises an error on a true bool" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" }) expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end end
it "parses an array" do it "raises an error on a false bool" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }]) expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: true)).to be(false)
end
end end
end end
describe ".dump" do context "feature flag is disabled" do
it "dumps an object" do before do
expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}') stub_feature_flags(json_wrapper_legacy_mode: false)
end end
it "dumps an array" do it "parses an object" do
expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]') expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end end
it "dumps a string" do it "parses an array" do
expect(subject.dump("foo")).to eq('"foo"') expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end end
it "dumps a true bool" do it "parses a string" do
expect(subject.dump(true)).to eq("true") expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
end end
it "dumps a false bool" do it "parses a true bool" do
expect(subject.dump(false)).to eq("false") expect(subject.parse("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: true)).to be(false)
end
end
end
describe ".parse!" do
context "legacy_mode is disabled by default" do
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: false)).to be(false)
end end
end end
describe ".generate" do context "legacy_mode is enabled" do
let(:obj) do it "parses an object" do
{ test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] } expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end end
it "generates JSON" do it "parses an array" do
expected_string = <<~STR.chomp expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
{"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]}
STR
expect(subject.generate(obj)).to eq(expected_string)
end end
it "allows you to customise the output" do it "raises an error on a string" do
opts = { expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
indent: " ", end
space: " ",
space_before: " ",
object_nl: "\n",
array_nl: "\n"
}
json = subject.generate(obj, opts) it "raises an error on a true bool" do
expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
expected_string = <<~STR.chomp it "raises an error on a false bool" do
{ expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
]
}
STR
expect(json).to eq(expected_string)
end end
end end
describe ".pretty_generate" do context "feature flag is disabled" do
let(:obj) do before do
stub_feature_flags(json_wrapper_legacy_mode: false)
end
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: true)).to be(false)
end
end
end
describe ".dump" do
it "dumps an object" do
expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}')
end
it "dumps an array" do
expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]')
end
it "dumps a string" do
expect(subject.dump("foo")).to eq('"foo"')
end
it "dumps a true bool" do
expect(subject.dump(true)).to eq("true")
end
it "dumps a false bool" do
expect(subject.dump(false)).to eq("false")
end
end
describe ".generate" do
let(:obj) do
{ test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] }
end
it "generates JSON" do
expected_string = <<~STR.chomp
{"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]}
STR
expect(subject.generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
indent: " ",
space: " ",
space_before: " ",
object_nl: "\n",
array_nl: "\n"
}
json = subject.generate(obj, opts)
expected_string = <<~STR.chomp
{ {
test: true, "test" : true,
"foo.bar" => "baz", "foo.bar" : "baz",
is_json: 1, "is_json" : 1,
some: [1, 2, 3], "some" : [
more: { test: true }, 1,
multi_line_empty_array: [], 2,
multi_line_empty_obj: {} 3
]
} }
end STR
it "generates pretty JSON" do expect(json).to eq(expected_string)
expected_string = <<~STR.chomp
{
"test": true,
"foo.bar": "baz",
"is_json": 1,
"some": [
1,
2,
3
],
"more": {
"test": true
},
"multi_line_empty_array": [
],
"multi_line_empty_obj": {
}
}
STR
expect(subject.pretty_generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
space_before: " "
}
json = subject.pretty_generate(obj, opts)
expected_string = <<~STR.chomp
{
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
],
"more" : {
"test" : true
},
"multi_line_empty_array" : [
],
"multi_line_empty_obj" : {
}
}
STR
expect(json).to eq(expected_string)
end
end
context "the feature table is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
it "skips legacy mode handling" do
expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true)
subject.send(:handle_legacy_mode!, {})
end
it "skips oj feature detection" do
expect(Feature).not_to receive(:enabled?).with(:oj_json, default_enabled: true)
subject.send(:enable_oj?)
end
end
context "the database is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad)
end
it "still parses json" do
expect(subject.parse("{}")).to eq({})
end
it "still generates json" do
expect(subject.dump({})).to eq("{}")
end
end end
end end
context "oj gem" do describe ".pretty_generate" do
before do let(:obj) do
stub_feature_flags(oj_json: true) {
test: true,
"foo.bar" => "baz",
is_json: 1,
some: [1, 2, 3],
more: { test: true },
multi_line_empty_array: [],
multi_line_empty_obj: {}
}
end end
it_behaves_like "json" it "generates pretty JSON" do
expected_string = <<~STR.chomp
{
"test": true,
"foo.bar": "baz",
"is_json": 1,
"some": [
1,
2,
3
],
"more": {
"test": true
},
"multi_line_empty_array": [
describe "#enable_oj?" do ],
it "returns true" do "multi_line_empty_obj": {
expect(subject.enable_oj?).to be(true) }
end }
STR
expect(subject.pretty_generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
space_before: " "
}
json = subject.pretty_generate(obj, opts)
expected_string = <<~STR.chomp
{
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
],
"more" : {
"test" : true
},
"multi_line_empty_array" : [
],
"multi_line_empty_obj" : {
}
}
STR
expect(json).to eq(expected_string)
end end
end end
context "json gem" do context "the feature table is missing" do
before do before do
stub_feature_flags(oj_json: false) allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end end
it_behaves_like "json" it "skips legacy mode handling" do
expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true)
describe "#enable_oj?" do subject.send(:handle_legacy_mode!, {})
it "returns false" do end
expect(subject.enable_oj?).to be(false) end
end
context "the database is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad)
end
it "still parses json" do
expect(subject.parse("{}")).to eq({})
end
it "still generates json" do
expect(subject.dump({})).to eq("{}")
end end
end end
@ -353,47 +317,25 @@ RSpec.describe Gitlab::Json do
let(:env) { {} } let(:env) { {} }
let(:result) { "{\"test\":true}" } let(:result) { "{\"test\":true}" }
context "oj is enabled" do context "grape_gitlab_json flag is enabled" do
before do before do
stub_feature_flags(oj_json: true) stub_feature_flags(grape_gitlab_json: true)
end end
context "grape_gitlab_json flag is enabled" do it "generates JSON" do
before do expect(subject).to eq(result)
stub_feature_flags(grape_gitlab_json: true)
end
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Gitlab::Json" do
expect(Gitlab::Json).to receive(:dump).with(obj)
subject
end
end end
context "grape_gitlab_json flag is disabled" do it "uses Gitlab::Json" do
before do expect(Gitlab::Json).to receive(:dump).with(obj)
stub_feature_flags(grape_gitlab_json: false)
end
it "generates JSON" do subject
expect(subject).to eq(result)
end
it "uses Grape::Formatter::Json" do
expect(Grape::Formatter::Json).to receive(:call).with(obj, env)
subject
end
end end
end end
context "oj is disabled" do context "grape_gitlab_json flag is disabled" do
before do before do
stub_feature_flags(oj_json: false) stub_feature_flags(grape_gitlab_json: false)
end end
it "generates JSON" do it "generates JSON" do

View File

@ -20,9 +20,8 @@ RSpec.describe PersonalSnippet do
it_behaves_like 'model with repository' do it_behaves_like 'model with repository' do
let_it_be(:container) { create(:personal_snippet, :repository) } let_it_be(:container) { create(:personal_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:personal_snippet) } let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" } let(:expected_full_path) { "snippets/#{container.id}" }
let(:expected_web_url_path) { "-/snippets/#{container.id}" } let(:expected_web_url_path) { "-/snippets/#{container.id}" }
let(:expected_repo_url_path) { "snippets/#{container.id}" }
end end
describe '#parent_user' do describe '#parent_user' do

View File

@ -36,8 +36,7 @@ RSpec.describe ProjectSnippet do
it_behaves_like 'model with repository' do it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project_snippet, :repository) } let_it_be(:container) { create(:project_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:project_snippet) } let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" } let(:expected_full_path) { "#{container.project.full_path}/snippets/#{container.id}" }
let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" } let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" }
let(:expected_repo_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
end end
end end