Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2b1e7f7dac
commit
f3b9e205bb
|
@ -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
|
||||||
|
|
|
@ -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) ..."
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
@ -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 }
|
|
@ -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?
|
|
@ -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) }
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Configure CSP for displaying Youtube videos in the Static Site Editor
|
||||||
|
merge_request: 45767
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Use CodeQuality 0.85.18 in the CI template
|
||||||
|
merge_request: 46253
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add environment variables to override backup/restore DB settings
|
||||||
|
merge_request: 45855
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
name: oj_json
|
|
||||||
introduced_by_url:
|
|
||||||
rollout_issue_url:
|
|
||||||
group:
|
|
||||||
type: development
|
|
||||||
default_enabled: true
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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: '<img onerror=alert(1)>')
|
milestone = build(:milestone, title: '<img onerror=alert(1)>')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue