Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3fd97b4bba
commit
cbfe03ae04
|
@ -12,6 +12,11 @@ stages:
|
|||
- post-qa
|
||||
- pages
|
||||
|
||||
# always use `gitlab-org` runners
|
||||
default:
|
||||
tags:
|
||||
- gitlab-org
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.run-dev-fixtures:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.review-docs:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .docs:rules:review-docs
|
||||
allow_failure: true
|
||||
|
@ -42,7 +41,6 @@ review-docs-cleanup:
|
|||
|
||||
docs lint:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .docs:rules:docs-lint
|
||||
image: "registry.gitlab.com/gitlab-org/gitlab-docs:lint"
|
||||
|
@ -64,7 +62,6 @@ docs lint:
|
|||
|
||||
graphql-reference-verify:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
.gitlab:assets:compile-metadata:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-before_script
|
||||
- .assets-compile-cache
|
||||
|
@ -64,7 +63,6 @@ gitlab:assets:compile pull-cache:
|
|||
|
||||
.compile-assets-metadata:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-before_script
|
||||
- .assets-compile-cache
|
||||
|
@ -122,7 +120,6 @@ compile-assets pull-cache as-if-foss:
|
|||
|
||||
.frontend-fixtures-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
@ -160,7 +157,6 @@ frontend-fixtures-as-if-foss:
|
|||
|
||||
.frontend-job-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
@ -242,7 +238,6 @@ jest-as-if-foss:
|
|||
|
||||
coverage-frontend:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .frontend:rules:default-frontend-jobs
|
||||
needs: ["jest"]
|
||||
|
@ -262,7 +257,6 @@ coverage-frontend:
|
|||
|
||||
.qa-frontend-node:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .frontend:rules:qa-frontend-node
|
||||
|
@ -290,7 +284,6 @@ qa-frontend-node:latest:
|
|||
|
||||
webpack-dev-server:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .frontend:rules:default-frontend-jobs
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.default-tags:
|
||||
tags:
|
||||
- gitlab-org
|
||||
|
||||
.default-retry:
|
||||
retry:
|
||||
max: 2 # This is confusing but this means "3 runs at max".
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.only-code-memory-job-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
pages:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .pages:rules
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.qa-job-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
stage: test
|
||||
needs: []
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
.rails-job-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.review-docker:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
|
||||
services:
|
||||
|
@ -29,7 +28,6 @@ build-qa-image:
|
|||
|
||||
review-cleanup:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .review:rules:review-cleanup
|
||||
stage: prepare
|
||||
|
@ -46,7 +44,6 @@ review-cleanup:
|
|||
|
||||
review-build-cng:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .review:rules:mr-and-schedule
|
||||
image: ruby:2.6-alpine
|
||||
|
@ -63,7 +60,6 @@ review-build-cng:
|
|||
|
||||
.review-workflow-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
|
||||
variables:
|
||||
|
@ -217,7 +213,6 @@ review-performance:
|
|||
|
||||
parallel-spec-reports:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .review:rules:mr-only-manual
|
||||
image: ruby:2.6-alpine
|
||||
stage: post-qa
|
||||
|
@ -244,7 +239,6 @@ parallel-spec-reports:
|
|||
|
||||
danger-review:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .review:rules:danger
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# rubygems.org in the future.
|
||||
cache gems:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-before_script
|
||||
|
@ -21,7 +20,6 @@ cache gems:
|
|||
|
||||
.minimal-job:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
needs: []
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# This uses rules from project root `.yamllint`.
|
||||
lint-ci-gitlab:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .yaml:rules
|
||||
image: sdesbure/yamllint:latest
|
||||
|
|
|
@ -59,6 +59,10 @@ See the test engineering planning process and reach out to your counterpart Soft
|
|||
|
||||
<!-- Which leads to: in which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#four-tiers -->
|
||||
|
||||
### Is this a cross-stage feature?
|
||||
|
||||
<!-- Communicate if this change will affect multiple Stage Groups or product areas. We recommend always start with the assumption that a feature request will have an impact into another Group. Loop in the most relevant PM and Product Designer from that Group to provide strategic support to help align the Group's broader plan and vision, as well as to avoid UX and technical debt. https://about.gitlab.com/handbook/product/#cross-stage-features -->
|
||||
|
||||
### Links / references
|
||||
|
||||
/label ~feature
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import flash from '~/flash';
|
||||
import $ from 'jquery';
|
||||
import { sprintf, __ } from '../../locale';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { once } from 'lodash';
|
||||
|
||||
// Renders diagrams and flowcharts from text using Mermaid in any element with the
|
||||
// `js-render-mermaid` class.
|
||||
|
@ -18,14 +19,10 @@ import { sprintf, __ } from '../../locale';
|
|||
|
||||
// This is an arbitrary number; Can be iterated upon when suitable.
|
||||
const MAX_CHAR_LIMIT = 5000;
|
||||
let mermaidModule = {};
|
||||
|
||||
function renderMermaids($els) {
|
||||
if (!$els.length) return;
|
||||
|
||||
// A diagram may have been truncated in search results which will cause errors, so abort the render.
|
||||
if (document.querySelector('body').dataset.page === 'search:show') return;
|
||||
|
||||
import(/* webpackChunkName: 'mermaid' */ 'mermaid')
|
||||
function importMermaidModule() {
|
||||
return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
|
||||
.then(mermaid => {
|
||||
mermaid.initialize({
|
||||
// mermaid core options
|
||||
|
@ -41,63 +38,127 @@ function renderMermaids($els) {
|
|||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
mermaidModule = mermaid;
|
||||
|
||||
return mermaid;
|
||||
})
|
||||
.catch(err => {
|
||||
flash(sprintf(__("Can't load mermaid module: %{err}"), { err }));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function fixElementSource(el) {
|
||||
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
|
||||
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
|
||||
|
||||
// Remove any extra spans added by the backend syntax highlighting.
|
||||
Object.assign(el, { textContent: source });
|
||||
|
||||
return { source };
|
||||
}
|
||||
|
||||
function renderMermaidEl(el) {
|
||||
mermaidModule.init(undefined, el, id => {
|
||||
const source = el.textContent;
|
||||
const svg = document.getElementById(id);
|
||||
|
||||
// As of https://github.com/knsv/mermaid/commit/57b780a0d,
|
||||
// Mermaid will make two init callbacks:one to initialize the
|
||||
// flow charts, and another to initialize the Gannt charts.
|
||||
// Guard against an error caused by double initialization.
|
||||
if (svg.classList.contains('mermaid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
svg.classList.add('mermaid');
|
||||
|
||||
// pre > code > svg
|
||||
svg.closest('pre').replaceWith(svg);
|
||||
|
||||
// We need to add the original source into the DOM to allow Copy-as-GFM
|
||||
// to access it.
|
||||
const sourceEl = document.createElement('text');
|
||||
sourceEl.classList.add('source');
|
||||
sourceEl.setAttribute('display', 'none');
|
||||
sourceEl.textContent = source;
|
||||
|
||||
svg.appendChild(sourceEl);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMermaids($els) {
|
||||
if (!$els.length) return;
|
||||
|
||||
// A diagram may have been truncated in search results which will cause errors, so abort the render.
|
||||
if (document.querySelector('body').dataset.page === 'search:show') return;
|
||||
|
||||
importMermaidModule()
|
||||
.then(() => {
|
||||
let renderedChars = 0;
|
||||
|
||||
$els.each((i, el) => {
|
||||
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
|
||||
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
|
||||
|
||||
const { source } = fixElementSource(el);
|
||||
/**
|
||||
* Restrict the rendering to a certain amount of character to
|
||||
* prevent mermaidjs from hanging up the entire thread and
|
||||
* causing a DoS.
|
||||
*/
|
||||
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
|
||||
el.textContent = sprintf(
|
||||
__(
|
||||
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
|
||||
),
|
||||
{ charLimit: MAX_CHAR_LIMIT },
|
||||
);
|
||||
const html = `
|
||||
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
|
||||
<div>
|
||||
<div class="display-flex">
|
||||
<div>${__(
|
||||
'Warning: Displaying this diagram might cause performance issues on this page.',
|
||||
)}</div>
|
||||
<div class="gl-alert-actions">
|
||||
<button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md new-gl-button">Display</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const $parent = $(el).parent();
|
||||
|
||||
if (!$parent.hasClass('lazy-alert-shown')) {
|
||||
$parent.after(html);
|
||||
$parent.addClass('lazy-alert-shown');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
renderedChars += source.length;
|
||||
// Remove any extra spans added by the backend syntax highlighting.
|
||||
Object.assign(el, { textContent: source });
|
||||
|
||||
mermaid.init(undefined, el, id => {
|
||||
const svg = document.getElementById(id);
|
||||
|
||||
// As of https://github.com/knsv/mermaid/commit/57b780a0d,
|
||||
// Mermaid will make two init callbacks:one to initialize the
|
||||
// flow charts, and another to initialize the Gannt charts.
|
||||
// Guard against an error caused by double initialization.
|
||||
if (svg.classList.contains('mermaid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
svg.classList.add('mermaid');
|
||||
|
||||
// pre > code > svg
|
||||
svg.closest('pre').replaceWith(svg);
|
||||
|
||||
// We need to add the original source into the DOM to allow Copy-as-GFM
|
||||
// to access it.
|
||||
const sourceEl = document.createElement('text');
|
||||
sourceEl.classList.add('source');
|
||||
sourceEl.setAttribute('display', 'none');
|
||||
sourceEl.textContent = source;
|
||||
|
||||
svg.appendChild(sourceEl);
|
||||
});
|
||||
renderMermaidEl(el);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
flash(`Can't load mermaid module: ${err}`);
|
||||
flash(sprintf(__('Encountered an error while rendering: %{err}'), { err }));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const hookLazyRenderMermaidEvent = once(() => {
|
||||
$(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
|
||||
const parent = $(this).closest('.js-lazy-render-mermaid-container');
|
||||
const pre = parent.prev();
|
||||
|
||||
const el = pre.find('.js-render-mermaid');
|
||||
|
||||
parent.remove();
|
||||
|
||||
renderMermaidEl(el);
|
||||
});
|
||||
});
|
||||
|
||||
export default function renderMermaid($els) {
|
||||
if (!$els.length) return;
|
||||
|
||||
|
@ -112,4 +173,6 @@ export default function renderMermaid($els) {
|
|||
renderMermaids($(this).find('.js-render-mermaid'));
|
||||
}
|
||||
});
|
||||
|
||||
hookLazyRenderMermaidEvent();
|
||||
}
|
||||
|
|
|
@ -308,6 +308,7 @@ export default {
|
|||
'is-added': file.tempFile,
|
||||
}"
|
||||
class="multi-file-editor-holder"
|
||||
data-qa-selector="editor_container"
|
||||
@focusout="triggerFilesChange"
|
||||
></div>
|
||||
<content-viewer
|
||||
|
|
|
@ -39,6 +39,7 @@ const populateUserInfo = user => {
|
|||
location: userData.location,
|
||||
bio: userData.bio,
|
||||
organization: userData.organization,
|
||||
jobTitle: userData.job_title,
|
||||
loaded: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ export default {
|
|||
data-placement="bottom"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
data-qa-selector="open_in_web_ide_button"
|
||||
>
|
||||
{{ s__('mrWidget|Open in Web IDE') }}
|
||||
</a>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { GlPopover, GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
|
||||
import { glEmojiTag } from '../../../emoji';
|
||||
import { s__ } from '~/locale';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export default {
|
||||
name: 'UserPopover',
|
||||
|
@ -10,6 +12,7 @@ export default {
|
|||
Icon,
|
||||
GlPopover,
|
||||
GlSkeletonLoading,
|
||||
GlSprintf,
|
||||
UserAvatarImage,
|
||||
},
|
||||
props: {
|
||||
|
@ -45,8 +48,27 @@ export default {
|
|||
nameIsLoading() {
|
||||
return !this.user.name;
|
||||
},
|
||||
jobInfoIsLoading() {
|
||||
return !this.user.loaded && this.user.organization === null;
|
||||
workInformationIsLoading() {
|
||||
return !this.user.loaded && this.workInformation === null;
|
||||
},
|
||||
workInformation() {
|
||||
const { jobTitle, organization } = this.user;
|
||||
|
||||
if (organization && jobTitle) {
|
||||
return {
|
||||
message: s__('Profile|%{job_title} at %{organization}'),
|
||||
placeholders: { job_title: jobTitle, organization },
|
||||
};
|
||||
} else if (organization) {
|
||||
return organization;
|
||||
} else if (jobTitle) {
|
||||
return jobTitle;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
workInformationShouldUseSprintf() {
|
||||
return !isString(this.workInformation);
|
||||
},
|
||||
locationIsLoading() {
|
||||
return !this.user.loaded && this.user.location === null;
|
||||
|
@ -72,16 +94,30 @@ export default {
|
|||
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<div v-if="user.bio" class="js-bio d-flex mb-1">
|
||||
<div v-if="user.bio" class="d-flex mb-1">
|
||||
<icon name="profile" class="category-icon flex-shrink-0" />
|
||||
<span class="ml-1">{{ user.bio }}</span>
|
||||
<span ref="bio" class="ml-1">{{ user.bio }}</span>
|
||||
</div>
|
||||
<div v-if="user.organization" class="js-organization d-flex mb-1">
|
||||
<icon v-show="!jobInfoIsLoading" name="work" class="category-icon flex-shrink-0" />
|
||||
<span class="ml-1">{{ user.organization }}</span>
|
||||
<div v-if="workInformation" class="d-flex mb-1">
|
||||
<icon
|
||||
v-show="!workInformationIsLoading"
|
||||
name="work"
|
||||
class="category-icon flex-shrink-0"
|
||||
/>
|
||||
<span ref="workInformation" class="ml-1">
|
||||
<gl-sprintf v-if="workInformationShouldUseSprintf" :message="workInformation.message">
|
||||
<template
|
||||
v-for="(placeholder, slotName) in workInformation.placeholders"
|
||||
v-slot:[slotName]
|
||||
>
|
||||
<span :key="slotName">{{ placeholder }}</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<span v-else>{{ workInformation }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<gl-skeleton-loading
|
||||
v-if="jobInfoIsLoading"
|
||||
v-if="workInformationIsLoading"
|
||||
:lines="1"
|
||||
class="animation-container-small mb-1"
|
||||
/>
|
||||
|
|
|
@ -161,13 +161,17 @@
|
|||
}
|
||||
|
||||
.cover-controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
@include media-breakpoint-up(sm) {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1.25rem;
|
||||
}
|
||||
|
||||
&.left {
|
||||
left: 10px;
|
||||
right: auto;
|
||||
@include media-breakpoint-up(sm) {
|
||||
left: 1.25rem;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -401,3 +401,21 @@
|
|||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@mixin middle-dot-divider {
|
||||
&::after {
|
||||
// Duplicate `content` property used as a fallback
|
||||
// scss-lint:disable DuplicateProperty
|
||||
content: '\00B7'; // middle dot fallback if browser does not support alternative content
|
||||
content: '\00B7' / ''; // tell screen readers to ignore the content https://www.w3.org/TR/css-content-3/#accessibility
|
||||
padding: 0 0.375rem;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
content: '';
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,17 +74,12 @@
|
|||
|
||||
// Middle dot divider between each element in a list of items.
|
||||
.middle-dot-divider {
|
||||
&::after {
|
||||
content: '\00B7'; // Middle Dot
|
||||
padding: 0 6px;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
@include middle-dot-divider;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
content: '';
|
||||
padding: 0;
|
||||
}
|
||||
.middle-dot-divider-sm {
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include middle-dot-divider;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,10 +197,6 @@
|
|||
}
|
||||
|
||||
.user-profile {
|
||||
.cover-controls a {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
margin: 0 $gl-padding;
|
||||
|
||||
|
|
|
@ -12,11 +12,7 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if release_params[:description].present?
|
||||
release.update(release_params)
|
||||
else
|
||||
release.destroy
|
||||
end
|
||||
release.update(release_params) if release.persisted? || release_params[:description].present?
|
||||
|
||||
redirect_to project_tag_path(@project, tag.name)
|
||||
end
|
||||
|
|
|
@ -66,7 +66,7 @@ module SubmoduleHelper
|
|||
project].join('')
|
||||
|
||||
url_with_dotgit = url_no_dotgit + '.git'
|
||||
url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join(''))
|
||||
url_with_dotgit == Gitlab::Shell.url_to_repo([namespace, '/', project].join(''))
|
||||
end
|
||||
|
||||
def relative_self_url?(url)
|
||||
|
|
|
@ -91,6 +91,21 @@ module UsersHelper
|
|||
end
|
||||
end
|
||||
|
||||
def work_information(user)
|
||||
return unless user
|
||||
|
||||
organization = user.organization
|
||||
job_title = user.job_title
|
||||
|
||||
if organization.present? && job_title.present?
|
||||
s_('Profile|%{job_title} at %{organization}') % { job_title: job_title, organization: organization }
|
||||
elsif job_title.present?
|
||||
job_title
|
||||
elsif organization.present?
|
||||
organization
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_profile_tabs
|
||||
|
|
|
@ -61,12 +61,13 @@ module BulkInsertSafe
|
|||
super
|
||||
end
|
||||
|
||||
# Inserts the given ActiveRecord [items] to the table mapped to this class via [InsertAll].
|
||||
# Inserts the given ActiveRecord [items] to the table mapped to this class.
|
||||
# Items will be inserted in batches of a given size, where insertion semantics are
|
||||
# "atomic across all batches", i.e. either all items will be inserted or none.
|
||||
# "atomic across all batches".
|
||||
#
|
||||
# @param [Boolean] validate Whether validations should run on [items]
|
||||
# @param [Integer] batch_size How many items should at most be inserted at once
|
||||
# @param [Boolean] skip_duplicates Marks duplicates as allowed, and skips inserting them
|
||||
# @param [Proc] handle_attributes Block that will receive each item attribute hash
|
||||
# prior to insertion for further processing
|
||||
#
|
||||
|
@ -75,26 +76,65 @@ module BulkInsertSafe
|
|||
# - [ActiveRecord::RecordInvalid] on entity validation failures
|
||||
# - [ActiveRecord::RecordNotUnique] on duplicate key errors
|
||||
#
|
||||
# @return true if all items succeeded to be inserted, throws otherwise.
|
||||
# @return true if operation succeeded, throws otherwise.
|
||||
#
|
||||
def bulk_insert!(items, validate: true, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
|
||||
return true if items.empty?
|
||||
def bulk_insert!(items, validate: true, skip_duplicates: false, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
|
||||
_bulk_insert_all!(items,
|
||||
validate: validate,
|
||||
on_duplicate: skip_duplicates ? :skip : :raise,
|
||||
unique_by: nil,
|
||||
batch_size: batch_size,
|
||||
&handle_attributes)
|
||||
end
|
||||
|
||||
_bulk_insert_in_batches(items, batch_size, validate, &handle_attributes)
|
||||
|
||||
true
|
||||
# Upserts the given ActiveRecord [items] to the table mapped to this class.
|
||||
# Items will be inserted or updated in batches of a given size,
|
||||
# where insertion semantics are "atomic across all batches".
|
||||
#
|
||||
# @param [Boolean] validate Whether validations should run on [items]
|
||||
# @param [Integer] batch_size How many items should at most be inserted at once
|
||||
# @param [Symbol/Array] unique_by Defines index or columns to use to consider item duplicate
|
||||
# @param [Proc] handle_attributes Block that will receive each item attribute hash
|
||||
# prior to insertion for further processing
|
||||
#
|
||||
# Unique indexes can be identified by columns or name:
|
||||
# - unique_by: :isbn
|
||||
# - unique_by: %i[ author_id name ]
|
||||
# - unique_by: :index_books_on_isbn
|
||||
#
|
||||
# Note that this method will throw on the following occasions:
|
||||
# - [PrimaryKeySetError] when primary keys are set on entities prior to insertion
|
||||
# - [ActiveRecord::RecordInvalid] on entity validation failures
|
||||
# - [ActiveRecord::RecordNotUnique] on duplicate key errors
|
||||
#
|
||||
# @return true if operation succeeded, throws otherwise.
|
||||
#
|
||||
def bulk_upsert!(items, unique_by:, validate: true, batch_size: DEFAULT_BATCH_SIZE, &handle_attributes)
|
||||
_bulk_insert_all!(items,
|
||||
validate: validate,
|
||||
on_duplicate: :update,
|
||||
unique_by: unique_by,
|
||||
batch_size: batch_size,
|
||||
&handle_attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _bulk_insert_in_batches(items, batch_size, validate_items, &handle_attributes)
|
||||
def _bulk_insert_all!(items, on_duplicate:, unique_by:, validate:, batch_size:, &handle_attributes)
|
||||
return true if items.empty?
|
||||
|
||||
transaction do
|
||||
items.each_slice(batch_size) do |item_batch|
|
||||
attributes = _bulk_insert_item_attributes(item_batch, validate_items, &handle_attributes)
|
||||
attributes = _bulk_insert_item_attributes(
|
||||
item_batch, validate, &handle_attributes)
|
||||
|
||||
insert_all!(attributes)
|
||||
ActiveRecord::InsertAll
|
||||
.new(self, attributes, on_duplicate: on_duplicate, unique_by: unique_by)
|
||||
.execute
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def _bulk_insert_item_attributes(items, validate_items)
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
# needs any special behavior.
|
||||
module HasRepository
|
||||
extend ActiveSupport::Concern
|
||||
include Gitlab::ShellAdapter
|
||||
include AfterCommitQueue
|
||||
include Referable
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
@ -78,7 +77,7 @@ module HasRepository
|
|||
end
|
||||
|
||||
def url_to_repo
|
||||
gitlab_shell.url_to_repo(full_path)
|
||||
Gitlab::Shell.url_to_repo(full_path)
|
||||
end
|
||||
|
||||
def ssh_url_to_repo
|
||||
|
|
|
@ -1460,13 +1460,14 @@ class Project < ApplicationRecord
|
|||
# Forked import is handled asynchronously
|
||||
return if forked? && !force
|
||||
|
||||
if gitlab_shell.create_project_repository(self)
|
||||
repository.after_create
|
||||
true
|
||||
else
|
||||
errors.add(:base, _('Failed to create repository via gitlab-shell'))
|
||||
false
|
||||
end
|
||||
repository.create_repository
|
||||
repository.after_create
|
||||
|
||||
true
|
||||
rescue => err
|
||||
Gitlab::ErrorTracking.track_exception(err, project: { id: id, full_path: full_path, disk_path: disk_path })
|
||||
errors.add(:base, _('Failed to create repository'))
|
||||
false
|
||||
end
|
||||
|
||||
def hook_attrs(backward: true)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectWiki
|
||||
include Gitlab::ShellAdapter
|
||||
include Storage::LegacyProjectWiki
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
MARKUPS = {
|
||||
'Markdown' => :markdown,
|
||||
|
@ -47,7 +47,7 @@ class ProjectWiki
|
|||
end
|
||||
|
||||
def url_to_repo
|
||||
gitlab_shell.url_to_repo(full_path)
|
||||
Gitlab::Shell.url_to_repo(full_path)
|
||||
end
|
||||
|
||||
def ssh_url_to_repo
|
||||
|
@ -64,14 +64,15 @@ class ProjectWiki
|
|||
|
||||
# Returns the Gitlab::Git::Wiki object.
|
||||
def wiki
|
||||
@wiki ||= begin
|
||||
gl_repository = Gitlab::GlRepository::WIKI.identifier_for_container(project)
|
||||
raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path)
|
||||
strong_memoize(:wiki) do
|
||||
repository.create_if_not_exists
|
||||
raise CouldNotCreateWikiError unless repository_exists?
|
||||
|
||||
create_repo!(raw_repository) unless raw_repository.exists?
|
||||
|
||||
Gitlab::Git::Wiki.new(raw_repository)
|
||||
Gitlab::Git::Wiki.new(repository.raw)
|
||||
end
|
||||
rescue => err
|
||||
Gitlab::ErrorTracking.track_exception(err, project_wiki: { project_id: project.id, full_path: full_path, disk_path: disk_path })
|
||||
raise CouldNotCreateWikiError
|
||||
end
|
||||
|
||||
def repository_exists?
|
||||
|
@ -193,14 +194,6 @@ class ProjectWiki
|
|||
|
||||
private
|
||||
|
||||
def create_repo!(raw_repository)
|
||||
gitlab_shell.create_wiki_repository(project)
|
||||
|
||||
raise CouldNotCreateWikiError unless raw_repository.exists?
|
||||
|
||||
repository.after_create
|
||||
end
|
||||
|
||||
def commit_details(action, message = nil, title = nil)
|
||||
commit_message = message.presence || default_message(action, title)
|
||||
git_user = Gitlab::Git::User.from_gitlab(@user)
|
||||
|
|
|
@ -24,7 +24,7 @@ class Release < ApplicationRecord
|
|||
|
||||
accepts_nested_attributes_for :links, allow_destroy: true
|
||||
|
||||
validates :description, :project, :tag, presence: true
|
||||
validates :project, :tag, presence: true
|
||||
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
|
||||
|
||||
scope :sorted, -> { order(released_at: :desc) }
|
||||
|
|
|
@ -22,7 +22,7 @@ module Ci
|
|||
begin
|
||||
retry_optimistic_lock(ref) do
|
||||
next false if ref.persisted? &&
|
||||
(ref.last_updated_by_pipeline_id || 0) >= pipeline.id
|
||||
(ref.last_updated_by_pipeline_id || 0) > pipeline.id
|
||||
|
||||
ref.update(status: next_status(ref.status, pipeline.status),
|
||||
last_updated_by_pipeline: pipeline)
|
||||
|
|
|
@ -52,11 +52,14 @@ module Projects
|
|||
checksum = repository.checksum
|
||||
|
||||
# Initialize a git repository on the target path
|
||||
gitlab_shell.create_repository(new_storage_key, raw_repository.relative_path, full_path)
|
||||
new_repository = Gitlab::Git::Repository.new(new_storage_key,
|
||||
raw_repository.relative_path,
|
||||
raw_repository.gl_repository,
|
||||
full_path)
|
||||
new_repository = Gitlab::Git::Repository.new(
|
||||
new_storage_key,
|
||||
raw_repository.relative_path,
|
||||
raw_repository.gl_repository,
|
||||
full_path
|
||||
)
|
||||
|
||||
new_repository.create_repository
|
||||
|
||||
new_repository.replicate(raw_repository)
|
||||
new_checksum = new_repository.checksum
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
%p
|
||||
GitLab Shell
|
||||
%span.float-right
|
||||
= Gitlab::Shell.new.version
|
||||
= Gitlab::Shell.version
|
||||
%p
|
||||
GitLab Workhorse
|
||||
%span.float-right
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- page_title "UI Development Kit", "Help"
|
||||
- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare."
|
||||
- link_classes = "flex-grow-1 mx-1 "
|
||||
|
||||
.gitlab-ui-dev-kit
|
||||
%h1 GitLab UI development kit
|
||||
|
@ -64,7 +65,12 @@
|
|||
Cover block for profile page with avatar, name and description
|
||||
%code .cover-block
|
||||
.example
|
||||
.cover-block
|
||||
.cover-block.user-cover-block
|
||||
= render layout: 'users/cover_controls' do
|
||||
= link_to '#', class: link_classes + 'btn btn-default' do
|
||||
= icon('pencil')
|
||||
= link_to '#', class: link_classes + 'btn btn-default' do
|
||||
= icon('rss')
|
||||
.avatar-holder
|
||||
= image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
|
||||
.cover-title
|
||||
|
@ -73,13 +79,6 @@
|
|||
.cover-desc.cgray
|
||||
= lorem
|
||||
|
||||
.cover-controls
|
||||
= link_to '#', class: 'btn btn-default' do
|
||||
= icon('pencil')
|
||||
|
||||
= link_to '#', class: 'btn btn-default' do
|
||||
= icon('rss')
|
||||
|
||||
%h2#lists Lists
|
||||
|
||||
.lead
|
||||
|
|
|
@ -90,7 +90,6 @@
|
|||
.row
|
||||
= render 'profiles/name', form: f, user: @user
|
||||
= f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
|
||||
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { prompt: _('Select your role') }, required: true, class: 'input-md'
|
||||
|
||||
= render_if_exists 'profiles/email_settings', form: f
|
||||
= f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
|
||||
|
@ -101,6 +100,7 @@
|
|||
= f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
|
||||
- else
|
||||
= f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg', placeholder: s_("Profiles|City, country")
|
||||
= f.text_field :job_title, class: 'input-md'
|
||||
= f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md', help: s_("Profiles|Who you represent or work for")
|
||||
= f.text_area :bio, label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
|
||||
%hr
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
|
||||
= yield
|
|
@ -1,4 +1,4 @@
|
|||
%p
|
||||
%p.mb-1.mb-sm-2.mt-2.mt-sm-3
|
||||
%span.middle-dot-divider
|
||||
@#{@user.username}
|
||||
- if can?(current_user, :read_user_profile, @user)
|
||||
|
|
|
@ -4,30 +4,31 @@
|
|||
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name
|
||||
- page_description @user.bio
|
||||
- header_title @user.name, user_path(@user)
|
||||
- link_classes = "flex-grow-1 mx-1 "
|
||||
|
||||
= content_for :meta_tags do
|
||||
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
|
||||
|
||||
.user-profile
|
||||
.cover-block.user-cover-block{ class: [('border-bottom' if profile_tabs.empty?)] }
|
||||
.cover-controls
|
||||
= render layout: 'users/cover_controls' do
|
||||
- if @user == current_user
|
||||
= link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
|
||||
= link_to profile_path, class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
|
||||
= icon('pencil')
|
||||
- elsif current_user
|
||||
- if @user.abuse_report
|
||||
%button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'),
|
||||
%button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
|
||||
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
|
||||
= icon('exclamation-circle')
|
||||
- else
|
||||
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
|
||||
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
|
||||
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
= icon('exclamation-circle')
|
||||
- if can?(current_user, :read_user_profile, @user)
|
||||
= link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
|
||||
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
|
||||
= icon('rss')
|
||||
- if current_user && current_user.admin?
|
||||
= link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'),
|
||||
= link_to [:admin, @user], class: link_classes + 'btn btn-default', title: s_('UserProfile|View user in admin area'),
|
||||
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= icon('users')
|
||||
|
||||
|
@ -51,10 +52,18 @@
|
|||
= emoji_icon(@user.status.emoji)
|
||||
= markdown_field(@user.status, :message)
|
||||
= render "users/profile_basic_info"
|
||||
.cover-desc.cgray
|
||||
- unless @user.public_email.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link'
|
||||
.cover-desc.cgray.mb-1.mb-sm-2
|
||||
- unless @user.location.blank?
|
||||
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mb-1.mb-sm-0
|
||||
= sprite_icon('location', size: 16, css_class: 'vertical-align-sub fgray')
|
||||
%span.vertical-align-middle
|
||||
= @user.location
|
||||
- unless work_information(@user).blank?
|
||||
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline
|
||||
= sprite_icon('work', size: 16, css_class: 'vertical-align-middle fgray')
|
||||
%span.vertical-align-middle
|
||||
= work_information(@user)
|
||||
.cover-desc.cgray.mb-1.mb-sm-2
|
||||
- unless @user.skype.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
= link_to "skype:#{@user.skype}", title: "Skype" do
|
||||
|
@ -64,24 +73,18 @@
|
|||
= link_to linkedin_url(@user), title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= icon('linkedin-square')
|
||||
- unless @user.twitter.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
.profile-link-holder.middle-dot-divider-sm
|
||||
= link_to twitter_url(@user), title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= icon('twitter-square')
|
||||
- unless @user.website_url.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
|
||||
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
|
||||
- unless @user.location.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
= sprite_icon('location', size: 16, css_class: 'vertical-align-sub')
|
||||
= @user.location
|
||||
- unless @user.organization.blank?
|
||||
.profile-link-holder.middle-dot-divider
|
||||
= sprite_icon('work', size: 16, css_class: 'vertical-align-sub')
|
||||
= @user.organization
|
||||
|
||||
- unless @user.public_email.blank?
|
||||
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
|
||||
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link'
|
||||
- if @user.bio.present?
|
||||
.cover-desc.cgray
|
||||
%p.profile-user-bio
|
||||
%p.profile-user-bio.font-italic
|
||||
= @user.bio
|
||||
|
||||
- unless profile_tabs.empty?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add CI template to deploy to ECS
|
||||
merge_request: 26371
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize Project related count with slack service
|
||||
merge_request: 26686
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Correctly send notification on pipeline retry
|
||||
merge_request: 26803
|
||||
author: Jacopo Beschi @jacopo-beschi
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add "Job Title" field in user settings and display on profile
|
||||
merge_request: 25155
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 27880 Make release notes optional and do not delete release when they are removed
|
||||
merge_request: 26231
|
||||
author: Pavlo Dudchenko
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug setting hook env with personal snippets
|
||||
merge_request: 27235
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add functionality to render individual mermaids
|
||||
merge_request: 26564
|
||||
author:
|
||||
type: changed
|
|
@ -11,6 +11,9 @@ Rails.application.configure do
|
|||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = false
|
||||
|
||||
# Show a warning when a large data set is loaded into memory
|
||||
config.active_record.warn_on_records_fetched_greater_than = 1000
|
||||
|
||||
# Print deprecation notices to the Rails logger
|
||||
config.active_support.deprecation = :log
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
unless Rails.env.test?
|
||||
required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
|
||||
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
|
||||
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version)
|
||||
|
||||
unless current_version.valid? && required_version <= current_version
|
||||
warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell."
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnProjectIdAndTypeToServices < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_services_on_project_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :services, [:project_id, :type]
|
||||
|
||||
remove_concurrent_index_by_name :services, INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :services, :project_id, name: INDEX_NAME
|
||||
|
||||
remove_concurrent_index :services, [:project_id, :type]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnCreatorIdAndCreatedAtToProjects < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_projects_on_creator_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :projects, [:creator_id, :created_at]
|
||||
|
||||
remove_concurrent_index_by_name :projects, INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :projects, :creator_id, name: INDEX_NAME
|
||||
|
||||
remove_concurrent_index :projects, [:creator_id, :created_at]
|
||||
end
|
||||
end
|
|
@ -3486,8 +3486,8 @@ ActiveRecord::Schema.define(version: 2020_03_12_163407) do
|
|||
t.index ["created_at", "id"], name: "index_projects_api_created_at_id_desc", order: { id: :desc }
|
||||
t.index ["created_at", "id"], name: "index_projects_api_vis20_created_at", where: "(visibility_level = 20)"
|
||||
t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
|
||||
t.index ["creator_id", "created_at"], name: "index_projects_on_creator_id_and_created_at"
|
||||
t.index ["creator_id", "created_at"], name: "index_projects_on_mirror_creator_id_created_at", where: "((mirror = true) AND (mirror_trigger_builds = true))"
|
||||
t.index ["creator_id"], name: "index_projects_on_creator_id"
|
||||
t.index ["description"], name: "index_projects_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["id", "repository_storage", "last_repository_updated_at"], name: "idx_projects_on_repository_storage_last_repository_updated_at"
|
||||
t.index ["id"], name: "index_on_id_partial_with_legacy_storage", where: "((storage_version < 2) OR (storage_version IS NULL))"
|
||||
|
@ -3961,7 +3961,7 @@ ActiveRecord::Schema.define(version: 2020_03_12_163407) do
|
|||
t.boolean "comment_on_event_enabled", default: true, null: false
|
||||
t.boolean "template", default: false
|
||||
t.boolean "instance", default: false, null: false
|
||||
t.index ["project_id"], name: "index_services_on_project_id"
|
||||
t.index ["project_id", "type"], name: "index_services_on_project_id_and_type"
|
||||
t.index ["template"], name: "index_services_on_template"
|
||||
t.index ["type", "instance"], name: "index_services_on_type_and_instance", unique: true, where: "(instance IS TRUE)"
|
||||
t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)"
|
||||
|
|
|
@ -328,7 +328,7 @@ POST /projects/:id/releases
|
|||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
|
||||
| `name` | string | no | The release name. |
|
||||
| `tag_name` | string | yes | The tag where the release will be created from. |
|
||||
| `description` | string | yes | The description of the release. You can use [Markdown](../../user/markdown.md). |
|
||||
| `description` | string | no | The description of the release. You can use [Markdown](../../user/markdown.md). |
|
||||
| `ref` | string | yes, if `tag_name` doesn't exist | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. |
|
||||
| `milestones` | array of string | no | The title of each milestone the release is associated with. |
|
||||
| `assets:links` | array of hash | no | An array of assets links. |
|
||||
|
|
|
@ -61,3 +61,61 @@ To do so, please make sure to [push your image into your ECR
|
|||
repository](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html)
|
||||
before referencing it in your `.gitlab-ci.yml` file and replace the `image`
|
||||
path to point to your ECR.
|
||||
|
||||
### Deploy your application to AWS Elastic Container Service (ECS)
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207962) in GitLab 12.9.
|
||||
|
||||
GitLab provides a series of [CI templates that you can include in your project](../yaml/README.md#include).
|
||||
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
|
||||
cluster, you can `include` the `Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
|
||||
|
||||
Before getting started with this process, you need a cluster on AWS ECS, as well as related
|
||||
components, like an ECS service, ECS task definition, a database on AWS RDS, etc.
|
||||
[Read more about AWS ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html).
|
||||
|
||||
After you're all set up on AWS ECS, follow these steps:
|
||||
|
||||
1. Make sure your AWS credentials are set up as environment variables for your
|
||||
project. You can follow [the steps above](#aws) to complete this setup.
|
||||
1. Add these variables to your project's `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
CI_AWS_ECS_CLUSTER: my-cluster
|
||||
CI_AWS_ECS_SERVICE: my-service
|
||||
CI_AWS_ECS_TASK_DEFINITION: my-task-definition
|
||||
```
|
||||
|
||||
Three variables are defined in this snippet:
|
||||
|
||||
- `CI_AWS_ECS_CLUSTER`: The name of your AWS ECS cluster that you're
|
||||
targeting for your deployments.
|
||||
- `CI_AWS_ECS_SERVICE`: The name of the targeted service tied to
|
||||
your AWS ECS cluster.
|
||||
- `CI_AWS_ECS_TASK_DEFINITION`: The name of the task definition tied
|
||||
to the service mentioned above.
|
||||
|
||||
You can find these names after selecting the targeted cluster on your [AWS ECS dashboard](https://console.aws.amazon.com/ecs/home):
|
||||
|
||||
![AWS ECS dashboard](../img/ecs_dashboard_v12_9.png)
|
||||
|
||||
1. Include this template in `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Deploy-ECS.gitlab-ci.yml
|
||||
```
|
||||
|
||||
The `Deploy-ECS` template ships with GitLab and is available [on
|
||||
GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml).
|
||||
|
||||
1. Commit and push your updated `.gitlab-ci.yml` to your project's repository, and you're done!
|
||||
|
||||
Your application Docker image will be rebuilt and pushed to the GitLab registry.
|
||||
Then the targeted task definition will be updated with the location of the new
|
||||
Docker image, and a new revision will be created in ECS as result.
|
||||
|
||||
Finally, your AWS ECS service will be updated with the new revision of the
|
||||
task definition, making the cluster pull the newest version of your
|
||||
application.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
|
@ -36,11 +36,7 @@ Additionally, if you need large repos or multiple forks for testing, please cons
|
|||
|
||||
The Elasticsearch integration depends on an external indexer. We ship an [indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). The user must trigger the initial indexing via a rake task but, after this is done, GitLab itself will trigger reindexing when required via `after_` callbacks on create, update, and destroy that are inherited from [/ee/app/models/concerns/elastic/application_versioned_search.rb](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/models/concerns/elastic/application_versioned_search.rb).
|
||||
|
||||
After initial indexing is complete, updates proceed in one of two ways, depending on the `:elastic_bulk_incremental_updates` feature flag.
|
||||
|
||||
If disabled, every create, update, or delete operation on an Elasticsearch-tracked model enqueues a new `ElasticIndexerWorker` Sidekiq job which takes care of updating just that document. This is quite inefficient.
|
||||
|
||||
If the feature flag is enabled, create, update, and delete operations for all models except projects (see [#207494](https://gitlab.com/gitlab-org/gitlab/issues/207494)) are tracked in a Redis [`ZSET`](https://redis.io/topics/data-types#sorted-sets) instead. A regular `sidekiq-cron` `ElasticIndexBulkCronWorker` processes this queue, updating many Elasticsearch documents at a time with the [Bulk Request API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html).
|
||||
After initial indexing is complete, create, update, and delete operations for all models except projects (see [#207494](https://gitlab.com/gitlab-org/gitlab/issues/207494)) are tracked in a Redis [`ZSET`](https://redis.io/topics/data-types#sorted-sets). A regular `sidekiq-cron` `ElasticIndexBulkCronWorker` processes this queue, updating many Elasticsearch documents at a time with the [Bulk Request API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html).
|
||||
|
||||
Search queries are generated by the concerns found in [ee/app/models/concerns/elastic](https://gitlab.com/gitlab-org/gitlab/tree/master/ee/app/models/concerns/elastic). These concerns are also in charge of access control, and have been a historic source of security bugs so please pay close attention to them!
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ module API
|
|||
|
||||
# Stores some Git-specific env thread-safely
|
||||
env = parse_env
|
||||
Gitlab::Git::HookEnv.set(gl_repository, env) if project
|
||||
Gitlab::Git::HookEnv.set(gl_repository, env) if container
|
||||
|
||||
actor.update_last_used_at!
|
||||
access_checker = access_checker_for(actor, params[:protocol])
|
||||
|
|
|
@ -46,7 +46,7 @@ module API
|
|||
params do
|
||||
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
|
||||
optional :name, type: String, desc: 'The name of the release'
|
||||
requires :description, type: String, desc: 'The release notes'
|
||||
optional :description, type: String, desc: 'The release notes'
|
||||
optional :ref, type: String, desc: 'The commit sha or branch name'
|
||||
optional :assets, type: Hash do
|
||||
optional :links, type: Array do
|
||||
|
|
|
@ -4,7 +4,6 @@ require 'yaml'
|
|||
|
||||
module Backup
|
||||
class Repository
|
||||
include Gitlab::ShellAdapter
|
||||
attr_reader :progress
|
||||
|
||||
def initialize(progress)
|
||||
|
@ -71,23 +70,14 @@ module Backup
|
|||
def restore
|
||||
Project.find_each(batch_size: 1000) do |project|
|
||||
progress.print " * #{project.full_path} ... "
|
||||
path_to_project_bundle = path_to_bundle(project)
|
||||
|
||||
project.repository.remove rescue nil
|
||||
restore_repo_success = nil
|
||||
|
||||
if File.exist?(path_to_project_bundle)
|
||||
restore_repo_success =
|
||||
begin
|
||||
project.repository.create_from_bundle(path_to_project_bundle)
|
||||
restore_custom_hooks(project)
|
||||
restore_repo_success = true
|
||||
rescue => e
|
||||
restore_repo_success = false
|
||||
progress.puts "Error: #{e}".color(:red)
|
||||
try_restore_repository(project)
|
||||
rescue => err
|
||||
progress.puts "Error: #{err}".color(:red)
|
||||
false
|
||||
end
|
||||
else
|
||||
restore_repo_success = gitlab_shell.create_project_repository(project)
|
||||
end
|
||||
|
||||
if restore_repo_success
|
||||
progress.puts "[DONE]".color(:green)
|
||||
|
@ -118,6 +108,20 @@ module Backup
|
|||
|
||||
protected
|
||||
|
||||
def try_restore_repository(project)
|
||||
path_to_project_bundle = path_to_bundle(project)
|
||||
project.repository.remove rescue nil
|
||||
|
||||
if File.exist?(path_to_project_bundle)
|
||||
project.repository.create_from_bundle(path_to_project_bundle)
|
||||
restore_custom_hooks(project)
|
||||
else
|
||||
project.repository.create_repository
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def path_to_bundle(project)
|
||||
File.join(backup_repos_path, project.disk_path + '.bundle')
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
stages:
|
||||
- build
|
||||
- test
|
||||
- review
|
||||
- deploy
|
||||
- production
|
||||
|
||||
include:
|
||||
- template: Jobs/Build.gitlab-ci.yml
|
||||
|
||||
.deploy_to_ecs:
|
||||
image: registry.gitlab.com/gitlab-org/cloud-deploy:latest
|
||||
script:
|
||||
- ecs update-task-definition
|
||||
|
||||
review:
|
||||
extends: .deploy_to_ecs
|
||||
stage: review
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
only:
|
||||
refs:
|
||||
- branches
|
||||
- tags
|
||||
except:
|
||||
refs:
|
||||
- master
|
||||
|
||||
production:
|
||||
extends: .deploy_to_ecs
|
||||
stage: production
|
||||
environment:
|
||||
name: production
|
||||
only:
|
||||
refs:
|
||||
- master
|
|
@ -6,8 +6,6 @@ require 'securerandom'
|
|||
|
||||
module Gitlab
|
||||
class Shell
|
||||
GITLAB_SHELL_ENV_VARS = %w(GIT_TERMINAL_PROMPT).freeze
|
||||
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
|
@ -36,8 +34,31 @@ module Gitlab
|
|||
.join('GITLAB_SHELL_VERSION')).strip
|
||||
end
|
||||
|
||||
# Return GitLab shell version
|
||||
#
|
||||
# @return [String] version
|
||||
def version
|
||||
@version ||= File.read(gitlab_shell_version_file).chomp if File.readable?(gitlab_shell_version_file)
|
||||
end
|
||||
|
||||
# Return a SSH url for a given project path
|
||||
#
|
||||
# @param [String] full_path project path (URL)
|
||||
# @return [String] SSH URL
|
||||
def url_to_repo(full_path)
|
||||
Gitlab.config.gitlab_shell.ssh_path_prefix + "#{full_path}.git"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gitlab_shell_path
|
||||
File.expand_path(Gitlab.config.gitlab_shell.path)
|
||||
end
|
||||
|
||||
def gitlab_shell_version_file
|
||||
File.join(gitlab_shell_path, 'VERSION')
|
||||
end
|
||||
|
||||
# Create (if necessary) and link the secret token file
|
||||
def generate_and_link_secret_token
|
||||
secret_file = Gitlab.config.gitlab_shell.secret_file
|
||||
|
@ -56,47 +77,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# Initialize a new project repository using a Project model
|
||||
#
|
||||
# @param [Project] project
|
||||
# @return [Boolean] whether repository could be created
|
||||
def create_project_repository(project)
|
||||
create_repository(project.repository_storage, project.disk_path, project.full_path)
|
||||
end
|
||||
|
||||
# Initialize a new wiki repository using a Project model
|
||||
#
|
||||
# @param [Project] project
|
||||
# @return [Boolean] whether repository could be created
|
||||
def create_wiki_repository(project)
|
||||
create_repository(project.repository_storage, project.wiki.disk_path, project.wiki.full_path)
|
||||
end
|
||||
|
||||
# Init new repository
|
||||
#
|
||||
# @example Create a repository
|
||||
# create_repository("default", "path/to/gitlab-ci", "gitlab/gitlab-ci")
|
||||
#
|
||||
# @param [String] storage the shard key
|
||||
# @param [String] disk_path project path on disk
|
||||
# @param [String] gl_project_path project name
|
||||
# @return [Boolean] whether repository could be created
|
||||
def create_repository(storage, disk_path, gl_project_path)
|
||||
relative_path = disk_path.dup
|
||||
relative_path << '.git' unless relative_path.end_with?('.git')
|
||||
|
||||
# During creation of a repository, gl_repository may not be known
|
||||
# because that depends on a yet-to-be assigned project ID in the
|
||||
# database (e.g. project-1234), so for now it is blank.
|
||||
repository = Gitlab::Git::Repository.new(storage, relative_path, '', gl_project_path)
|
||||
wrapped_gitaly_errors { repository.gitaly_repository_client.create_repository }
|
||||
|
||||
true
|
||||
rescue => err # Once the Rugged codes gets removes this can be improved
|
||||
Rails.logger.error("Failed to add repository #{storage}/#{disk_path}: #{err}") # rubocop:disable Gitlab/RailsLogger
|
||||
false
|
||||
end
|
||||
|
||||
# Import wiki repository from external service
|
||||
#
|
||||
# @param [Project] project
|
||||
|
@ -238,25 +218,6 @@ module Gitlab
|
|||
false
|
||||
end
|
||||
|
||||
# Return a SSH url for a given project path
|
||||
#
|
||||
# @param [String] full_path project path (URL)
|
||||
# @return [String] SSH URL
|
||||
def url_to_repo(full_path)
|
||||
Gitlab.config.gitlab_shell.ssh_path_prefix + "#{full_path}.git"
|
||||
end
|
||||
|
||||
# Return GitLab shell version
|
||||
#
|
||||
# @return [String] version
|
||||
def version
|
||||
gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION"
|
||||
|
||||
if File.readable?(gitlab_shell_version_file)
|
||||
File.read(gitlab_shell_version_file).chomp
|
||||
end
|
||||
end
|
||||
|
||||
# Check if repository exists on disk
|
||||
#
|
||||
# @example Check if repository exists
|
||||
|
@ -271,23 +232,8 @@ module Gitlab
|
|||
false
|
||||
end
|
||||
|
||||
# Return hooks folder path used by projects
|
||||
#
|
||||
# @return [String] path
|
||||
def hooks_path
|
||||
File.join(gitlab_shell_path, 'hooks')
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def gitlab_shell_path
|
||||
File.expand_path(Gitlab.config.gitlab_shell.path)
|
||||
end
|
||||
|
||||
def gitlab_shell_user_home
|
||||
File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}")
|
||||
end
|
||||
|
||||
def full_path(storage, dir_name)
|
||||
raise ArgumentError.new("Directory name can't be blank") if dir_name.blank?
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ module SystemCheck
|
|||
end
|
||||
|
||||
def gitlab_shell_version
|
||||
Gitlab::Shell.new.version
|
||||
Gitlab::Shell.version
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,15 +82,10 @@ namespace :gitlab do
|
|||
puts "Using Omniauth:\t#{Gitlab::Auth.omniauth_enabled? ? "yes".color(:green) : "no"}"
|
||||
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab::Auth.omniauth_enabled?
|
||||
|
||||
# check Gitolite version
|
||||
gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.path}/VERSION"
|
||||
if File.readable?(gitlab_shell_version_file)
|
||||
gitlab_shell_version = File.read(gitlab_shell_version_file)
|
||||
end
|
||||
|
||||
# check Gitlab Shell version
|
||||
puts ""
|
||||
puts "GitLab Shell".color(:yellow)
|
||||
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
|
||||
puts "Version:\t#{Gitlab::Shell.version || "unknown".color(:red)}"
|
||||
puts "Repository storage paths:"
|
||||
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
||||
Gitlab.config.repositories.storages.each do |name, repository_storage|
|
||||
|
|
|
@ -3256,6 +3256,9 @@ msgstr ""
|
|||
msgid "Can't find variable: ZiteReader"
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't load mermaid module: %{err}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Can't remove group members without group managed account"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3304,9 +3307,6 @@ msgstr ""
|
|||
msgid "Cannot refer to a group milestone by an internal id!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7492,6 +7492,9 @@ msgstr ""
|
|||
msgid "Enabling this will only make licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public."
|
||||
msgstr ""
|
||||
|
||||
msgid "Encountered an error while rendering: %{err}"
|
||||
msgstr ""
|
||||
|
||||
msgid "End date"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8380,7 +8383,7 @@ msgstr ""
|
|||
msgid "Failed to create a branch for this issue. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create repository via gitlab-shell"
|
||||
msgid "Failed to create repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to create resources"
|
||||
|
@ -15148,6 +15151,9 @@ msgstr ""
|
|||
msgid "Profiles|your account"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profile|%{job_title} at %{organization}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiling - Performance bar"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17679,9 +17685,6 @@ msgstr ""
|
|||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select your role"
|
||||
msgstr ""
|
||||
|
||||
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22362,6 +22365,9 @@ msgstr ""
|
|||
msgid "Warning:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Warning: Displaying this diagram might cause performance issues on this page."
|
||||
msgstr ""
|
||||
|
||||
msgid "We could not determine the path to remove the epic"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
"katex": "^0.10.0",
|
||||
"lodash": "^4.17.15",
|
||||
"marked": "^0.3.12",
|
||||
"mermaid": "^8.4.5",
|
||||
"mermaid": "^8.4.8",
|
||||
"monaco-editor": "^0.18.1",
|
||||
"monaco-editor-webpack-plugin": "^1.7.0",
|
||||
"mousetrap": "^1.4.6",
|
||||
|
|
|
@ -14,6 +14,7 @@ module QA
|
|||
element :dropdown_toggle
|
||||
element :download_email_patches
|
||||
element :download_plain_diff
|
||||
element :open_in_web_ide_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue' do
|
||||
|
@ -219,6 +220,10 @@ module QA
|
|||
def wait_for_loading
|
||||
finished_loading? && has_no_element?(:skeleton_note)
|
||||
end
|
||||
|
||||
def click_open_in_web_ide
|
||||
click_element :open_in_web_ide_button
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,6 +48,10 @@ module QA
|
|||
element :start_new_mr_checkbox
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ide/components/repo_editor.vue' do
|
||||
element :editor_container
|
||||
end
|
||||
|
||||
def has_file?(file_name)
|
||||
within_element(:file_list) do
|
||||
page.has_content? file_name
|
||||
|
@ -113,6 +117,17 @@ module QA
|
|||
|
||||
raise "The changes do not appear to have been committed successfully." unless commit_success_msg_shown
|
||||
end
|
||||
|
||||
def add_to_modified_content(content)
|
||||
finished_loading?
|
||||
modified_text_area.set content
|
||||
end
|
||||
|
||||
def modified_text_area
|
||||
within_element(:editor_container) do
|
||||
find('.modified textarea.inputarea')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create', quarantine: { type: :new } do
|
||||
describe 'Review a merge request in Web IDE' do
|
||||
let(:new_file) { 'awesome_new_file.txt' }
|
||||
let(:review_text) { 'Reviewed ' }
|
||||
|
||||
let(:merge_request) do
|
||||
Resource::MergeRequest.fabricate_via_api! do |mr|
|
||||
mr.file_name = new_file
|
||||
mr.file_content = 'Text'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
|
||||
merge_request.visit!
|
||||
end
|
||||
|
||||
it 'opens and edits a merge request in Web IDE' do
|
||||
Page::MergeRequest::Show.perform do |show|
|
||||
show.click_open_in_web_ide
|
||||
end
|
||||
|
||||
Page::Project::WebIDE::Edit.perform do |ide|
|
||||
ide.has_file?(new_file)
|
||||
ide.add_to_modified_content(review_text)
|
||||
ide.commit_changes
|
||||
end
|
||||
|
||||
merge_request.visit!
|
||||
|
||||
Page::MergeRequest::Show.perform do |show|
|
||||
show.click_diffs_tab
|
||||
expect(show).to have_content(review_text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -67,13 +67,13 @@ describe Projects::Tags::ReleasesController do
|
|||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
it 'deletes release when description is empty' do
|
||||
initial_releases_count = project.releases.count
|
||||
it 'does not delete release when description is empty' do
|
||||
expect do
|
||||
update_release(tag, "")
|
||||
end.not_to change { project.releases.count }
|
||||
|
||||
response = update_release(release.tag, "")
|
||||
expect(release.reload.description).to eq("")
|
||||
|
||||
expect(initial_releases_count).to eq(1)
|
||||
expect(project.releases.count).to eq(0)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
|
|
|
@ -38,7 +38,9 @@ describe 'Mermaid rendering', :js do
|
|||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
expected = '<text><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
|
||||
wait_for_requests
|
||||
|
||||
expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
|
||||
expect(page.html.scan(expected).count).to be(4)
|
||||
end
|
||||
|
||||
|
@ -121,4 +123,40 @@ describe 'Mermaid rendering', :js do
|
|||
expect(svg[:width].to_i).to eq(100)
|
||||
expect(svg[:height].to_i).to eq(0)
|
||||
end
|
||||
|
||||
it 'display button when diagram exceeds length', :js do
|
||||
graph_edges = "A-->B;B-->A;" * 420
|
||||
|
||||
description = <<~MERMAID
|
||||
```mermaid
|
||||
graph LR
|
||||
#{graph_edges}
|
||||
```
|
||||
MERMAID
|
||||
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, project: project, description: description)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
page.within('.description') do
|
||||
expect(page).not_to have_selector('svg')
|
||||
|
||||
expect(page).to have_selector('pre.mermaid')
|
||||
|
||||
expect(page).to have_selector('.lazy-alert-shown')
|
||||
|
||||
expect(page).to have_selector('.js-lazy-render-mermaid-container')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-lazy-render-mermaid').click
|
||||
|
||||
page.within('.description') do
|
||||
expect(page).to have_selector('svg')
|
||||
|
||||
expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,11 @@ describe 'User edit profile' do
|
|||
wait_for_requests if respond_to?(:wait_for_requests)
|
||||
end
|
||||
|
||||
def visit_user
|
||||
visit user_path(user)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'changes user profile' do
|
||||
fill_in 'user_skype', with: 'testskype'
|
||||
fill_in 'user_linkedin', with: 'testlinkedin'
|
||||
|
@ -22,8 +27,8 @@ describe 'User edit profile' do
|
|||
fill_in 'user_website_url', with: 'testurl'
|
||||
fill_in 'user_location', with: 'Ukraine'
|
||||
fill_in 'user_bio', with: 'I <3 GitLab'
|
||||
fill_in 'user_job_title', with: 'Frontend Engineer'
|
||||
fill_in 'user_organization', with: 'GitLab'
|
||||
select 'Data Analyst', from: 'user_role'
|
||||
submit_settings
|
||||
|
||||
expect(user.reload).to have_attributes(
|
||||
|
@ -32,8 +37,8 @@ describe 'User edit profile' do
|
|||
twitter: 'testtwitter',
|
||||
website_url: 'testurl',
|
||||
bio: 'I <3 GitLab',
|
||||
organization: 'GitLab',
|
||||
role: 'data_analyst'
|
||||
job_title: 'Frontend Engineer',
|
||||
organization: 'GitLab'
|
||||
)
|
||||
|
||||
expect(find('#user_location').value).to eq 'Ukraine'
|
||||
|
@ -94,11 +99,6 @@ describe 'User edit profile' do
|
|||
end
|
||||
|
||||
context 'user status', :js do
|
||||
def visit_user
|
||||
visit user_path(user)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def select_emoji(emoji_name, is_modal = false)
|
||||
emoji_menu_class = is_modal ? '.js-modal-status-emoji-menu' : '.js-status-emoji-menu'
|
||||
toggle_button = find('.js-toggle-emoji-menu')
|
||||
|
@ -381,4 +381,40 @@ describe 'User edit profile' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'work information', :js do
|
||||
context 'when job title and organziation are entered' do
|
||||
it "shows job title and organzation on user's profile" do
|
||||
fill_in 'user_job_title', with: 'Frontend Engineer'
|
||||
fill_in 'user_organization', with: 'GitLab - work info test'
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
||||
expect(page).to have_content('Frontend Engineer at GitLab - work info test')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only job title is entered' do
|
||||
it "shows only job title on user's profile" do
|
||||
fill_in 'user_job_title', with: 'Frontend Engineer - work info test'
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
||||
expect(page).to have_content('Frontend Engineer - work info test')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only organization is entered' do
|
||||
it "shows only organization on user's profile" do
|
||||
fill_in 'user_organization', with: 'GitLab - work info test'
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
||||
expect(page).to have_content('GitLab - work info test')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,34 @@ describe 'User page' do
|
|||
|
||||
expect(page).not_to have_content("This user has a private profile")
|
||||
end
|
||||
|
||||
context 'work information' do
|
||||
subject { visit(user_path(user)) }
|
||||
|
||||
it 'shows job title and organization details' do
|
||||
user.update(organization: 'GitLab - work info test', job_title: 'Frontend Engineer')
|
||||
|
||||
subject
|
||||
|
||||
expect(page).to have_content('Frontend Engineer at GitLab - work info test')
|
||||
end
|
||||
|
||||
it 'shows job title' do
|
||||
user.update(organization: nil, job_title: 'Frontend Engineer - work info test')
|
||||
|
||||
subject
|
||||
|
||||
expect(page).to have_content('Frontend Engineer - work info test')
|
||||
end
|
||||
|
||||
it 'shows organization details' do
|
||||
user.update(organization: 'GitLab - work info test', job_title: '')
|
||||
|
||||
subject
|
||||
|
||||
expect(page).to have_content('GitLab - work info test')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private profile' do
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
@ -11,6 +11,7 @@ const DEFAULT_PROPS = {
|
|||
location: 'Vienna',
|
||||
bio: null,
|
||||
organization: null,
|
||||
jobTitle: null,
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
|
@ -39,6 +40,9 @@ describe('User Popover Component', () => {
|
|||
target: findTarget(),
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
'gl-sprintf': GlSprintf,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
@ -56,6 +60,7 @@ describe('User Popover Component', () => {
|
|||
location: null,
|
||||
bio: null,
|
||||
organization: null,
|
||||
jobTitle: null,
|
||||
status: null,
|
||||
},
|
||||
},
|
||||
|
@ -85,51 +90,125 @@ describe('User Popover Component', () => {
|
|||
});
|
||||
|
||||
describe('job data', () => {
|
||||
it('should show only bio if no organization is available', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, bio: 'Engineer' };
|
||||
const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
|
||||
const findBio = () => wrapper.find({ ref: 'bio' });
|
||||
|
||||
it('should show only bio if organization and job title are not available', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.text()).toContain('Engineer');
|
||||
expect(findBio().text()).toBe('My super interesting bio');
|
||||
expect(findWorkInformation().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show only organization if no bio is available', () => {
|
||||
it('should show only organization if job title is not available', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, organization: 'GitLab' };
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.text()).toContain('GitLab');
|
||||
expect(findWorkInformation().text()).toBe('GitLab');
|
||||
});
|
||||
|
||||
it('should display bio and organization in separate lines', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, bio: 'Engineer', organization: 'GitLab' };
|
||||
it('should show only job title if organization is not available', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, jobTitle: 'Frontend Engineer' };
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.find('.js-bio').text()).toContain('Engineer');
|
||||
expect(wrapper.find('.js-organization').text()).toContain('GitLab');
|
||||
expect(findWorkInformation().text()).toBe('Frontend Engineer');
|
||||
});
|
||||
|
||||
it('should not encode special characters in bio and organization', () => {
|
||||
it('should show organization and job title if they are both available', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
organization: 'GitLab',
|
||||
jobTitle: 'Frontend Engineer',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
|
||||
});
|
||||
|
||||
it('should display bio and job info in separate lines', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'My super interesting bio',
|
||||
organization: 'GitLab',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findBio().text()).toBe('My super interesting bio');
|
||||
expect(findWorkInformation().text()).toBe('GitLab');
|
||||
});
|
||||
|
||||
it('should not encode special characters in bio', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'I like <html> & CSS',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findBio().text()).toBe('I like <html> & CSS');
|
||||
});
|
||||
|
||||
it('should not encode special characters in organization', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'Manager & Team Lead',
|
||||
organization: 'Me & my <funky> Company',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
|
||||
expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company');
|
||||
expect(findWorkInformation().text()).toBe('Me & my <funky> Company');
|
||||
});
|
||||
|
||||
it('should not encode special characters in job title', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
jobTitle: 'Manager & Team Lead',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findWorkInformation().text()).toBe('Manager & Team Lead');
|
||||
});
|
||||
|
||||
it('should not encode special characters when both job title and organization are set', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
jobTitle: 'Manager & Team Lead',
|
||||
organization: 'Me & my <funky> Company',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findWorkInformation().text()).toBe('Manager & Team Lead at Me & my <funky> Company');
|
||||
});
|
||||
|
||||
it('shows icon for bio', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
bio: 'My super interesting bio',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows icon for organization', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
organization: 'GitLab',
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -178,4 +178,42 @@ describe UsersHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#work_information' do
|
||||
subject { helper.work_information(user) }
|
||||
|
||||
context 'when both job_title and organization are present' do
|
||||
let(:user) { build(:user, organization: 'GitLab', job_title: 'Frontend Engineer') }
|
||||
|
||||
it 'returns job title concatenated with organization' do
|
||||
is_expected.to eq('Frontend Engineer at GitLab')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only organization is present' do
|
||||
let(:user) { build(:user, organization: 'GitLab') }
|
||||
|
||||
it "returns organization" do
|
||||
is_expected.to eq('GitLab')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only job_title is present' do
|
||||
let(:user) { build(:user, job_title: 'Frontend Engineer') }
|
||||
|
||||
it 'returns job title' do
|
||||
is_expected.to eq('Frontend Engineer')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when neither organization nor job_title are present' do
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when user parameter is nil' do
|
||||
let(:user) { nil }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,9 +50,9 @@ describe Backup::Repository do
|
|||
|
||||
describe 'command failure' do
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::Shell) do |instance|
|
||||
allow(instance).to receive(:create_repository).and_return(false)
|
||||
end
|
||||
# Allow us to set expectations on the project directly
|
||||
expect(Project).to receive(:find_each).and_yield(project)
|
||||
expect(project.repository).to receive(:create_repository) { raise 'Fail in tests' }
|
||||
end
|
||||
|
||||
context 'hashed storage' do
|
||||
|
|
|
@ -173,10 +173,6 @@ describe Gitlab::LegacyGithubImport::Importer do
|
|||
]
|
||||
}
|
||||
|
||||
unless project.gitea_import?
|
||||
error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" }
|
||||
end
|
||||
|
||||
described_class.new(project).execute
|
||||
|
||||
expect(project.import_state.last_error).to eq error.to_json
|
||||
|
|
|
@ -7,18 +7,17 @@ describe Gitlab::Shell do
|
|||
let_it_be(:project) { create(:project, :repository) }
|
||||
let(:repository) { project.repository }
|
||||
let(:gitlab_shell) { described_class.new }
|
||||
let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } }
|
||||
let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
|
||||
|
||||
before do
|
||||
allow(Project).to receive(:find).and_return(project)
|
||||
end
|
||||
|
||||
it { is_expected.to respond_to :create_repository }
|
||||
it { is_expected.to respond_to :remove_repository }
|
||||
it { is_expected.to respond_to :fork_repository }
|
||||
|
||||
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
|
||||
describe '.url_to_repo' do
|
||||
let(:full_path) { 'diaspora/disaspora-rails' }
|
||||
|
||||
subject { described_class.url_to_repo(full_path) }
|
||||
|
||||
it { is_expected.to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + full_path + '.git') }
|
||||
end
|
||||
|
||||
describe 'memoized secret_token' do
|
||||
let(:secret_file) { 'tmp/tests/.secret_shell_test' }
|
||||
|
@ -49,37 +48,12 @@ describe Gitlab::Shell do
|
|||
describe 'projects commands' do
|
||||
let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') }
|
||||
let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') }
|
||||
let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') }
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
|
||||
allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
|
||||
end
|
||||
|
||||
describe '#create_repository' do
|
||||
let(:repository_storage) { 'default' }
|
||||
let(:repository_storage_path) do
|
||||
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
||||
Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
|
||||
end
|
||||
end
|
||||
let(:repo_name) { 'project/path' }
|
||||
let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(created_path)
|
||||
end
|
||||
|
||||
it 'returns false when the command fails' do
|
||||
FileUtils.mkdir_p(File.dirname(created_path))
|
||||
# This file will block the creation of the repo's .git directory. That
|
||||
# should cause #create_repository to fail.
|
||||
FileUtils.touch(created_path)
|
||||
|
||||
expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#remove_repository' do
|
||||
let!(:project) { create(:project, :repository, :legacy_storage) }
|
||||
let(:disk_path) { "#{project.disk_path}.git" }
|
||||
|
|
|
@ -22,6 +22,18 @@ describe BulkInsertSafe do
|
|||
algorithm: 'aes-256-gcm',
|
||||
key: Settings.attr_encrypted_db_key_base_32,
|
||||
insecure_mode: false
|
||||
|
||||
default_value_for :enum_value, 'case_1'
|
||||
default_value_for :secret_value, 'my-secret'
|
||||
default_value_for :sha_value, '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
|
||||
|
||||
def self.valid_list(count)
|
||||
Array.new(count) { |n| new(name: "item-#{n}") }
|
||||
end
|
||||
|
||||
def self.invalid_list(count)
|
||||
Array.new(count) { new }
|
||||
end
|
||||
end
|
||||
|
||||
module InheritedUnsafeMethods
|
||||
|
@ -48,6 +60,8 @@ describe BulkInsertSafe do
|
|||
t.text :encrypted_secret_value, null: false
|
||||
t.string :encrypted_secret_value_iv, null: false
|
||||
t.binary :sha_value, null: false, limit: 20
|
||||
|
||||
t.index :name, unique: true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -60,87 +74,95 @@ describe BulkInsertSafe do
|
|||
end
|
||||
end
|
||||
|
||||
def build_valid_items_for_bulk_insertion
|
||||
Array.new(10) do |n|
|
||||
BulkInsertItem.new(
|
||||
name: "item-#{n}",
|
||||
enum_value: 'case_1',
|
||||
secret_value: 'my-secret',
|
||||
sha_value: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_invalid_items_for_bulk_insertion
|
||||
Array.new(10) do
|
||||
BulkInsertItem.new(
|
||||
name: nil, # requires `name` to be set
|
||||
enum_value: 'case_1',
|
||||
secret_value: 'my-secret',
|
||||
sha_value: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a BulkInsertSafe model', BulkInsertItem do
|
||||
let(:valid_items_for_bulk_insertion) { build_valid_items_for_bulk_insertion }
|
||||
let(:invalid_items_for_bulk_insertion) { build_invalid_items_for_bulk_insertion }
|
||||
end
|
||||
|
||||
context 'when inheriting class methods' do
|
||||
it 'raises an error when method is not bulk-insert safe' do
|
||||
expect { BulkInsertItem.include(InheritedUnsafeMethods) }.to(
|
||||
raise_error(subject::MethodNotAllowedError))
|
||||
describe BulkInsertItem do
|
||||
it_behaves_like 'a BulkInsertSafe model', described_class do
|
||||
let(:valid_items_for_bulk_insertion) { described_class.valid_list(10) }
|
||||
let(:invalid_items_for_bulk_insertion) { described_class.invalid_list(10) }
|
||||
end
|
||||
|
||||
it 'does not raise an error when method is bulk-insert safe' do
|
||||
expect { BulkInsertItem.include(InheritedSafeMethods) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'primary keys' do
|
||||
it 'raises error if primary keys are set prior to insertion' do
|
||||
items = build_valid_items_for_bulk_insertion
|
||||
items.each_with_index do |item, n|
|
||||
item.id = n
|
||||
context 'when inheriting class methods' do
|
||||
it 'raises an error when method is not bulk-insert safe' do
|
||||
expect { described_class.include(InheritedUnsafeMethods) }
|
||||
.to raise_error(described_class::MethodNotAllowedError)
|
||||
end
|
||||
|
||||
expect { BulkInsertItem.bulk_insert!(items) }.to raise_error(subject::PrimaryKeySetError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.bulk_insert!' do
|
||||
it 'inserts items in the given number of batches' do
|
||||
items = build_valid_items_for_bulk_insertion
|
||||
expect(items.size).to eq(10)
|
||||
expect(BulkInsertItem).to receive(:insert_all!).twice
|
||||
|
||||
BulkInsertItem.bulk_insert!(items, batch_size: 5)
|
||||
it 'does not raise an error when method is bulk-insert safe' do
|
||||
expect { described_class.include(InheritedSafeMethods) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
it 'items can be properly fetched from database' do
|
||||
items = build_valid_items_for_bulk_insertion
|
||||
context 'primary keys' do
|
||||
it 'raises error if primary keys are set prior to insertion' do
|
||||
item = described_class.new(name: 'valid', id: 10)
|
||||
|
||||
BulkInsertItem.bulk_insert!(items)
|
||||
|
||||
attribute_names = BulkInsertItem.attribute_names - %w[id]
|
||||
expect(BulkInsertItem.last(items.size).pluck(*attribute_names)).to eq(
|
||||
items.pluck(*attribute_names))
|
||||
expect { described_class.bulk_insert!([item]) }
|
||||
.to raise_error(described_class::PrimaryKeySetError)
|
||||
end
|
||||
end
|
||||
|
||||
it 'rolls back the transaction when any item is invalid' do
|
||||
# second batch is bad
|
||||
all_items = build_valid_items_for_bulk_insertion + build_invalid_items_for_bulk_insertion
|
||||
batch_size = all_items.size / 2
|
||||
describe '.bulk_insert!' do
|
||||
it 'inserts items in the given number of batches' do
|
||||
items = described_class.valid_list(10)
|
||||
|
||||
expect do
|
||||
BulkInsertItem.bulk_insert!(all_items, batch_size: batch_size) rescue nil
|
||||
end.not_to change { BulkInsertItem.count }
|
||||
expect(ActiveRecord::InsertAll).to receive(:new).twice.and_call_original
|
||||
|
||||
described_class.bulk_insert!(items, batch_size: 5)
|
||||
end
|
||||
|
||||
it 'items can be properly fetched from database' do
|
||||
items = described_class.valid_list(10)
|
||||
|
||||
described_class.bulk_insert!(items)
|
||||
|
||||
attribute_names = described_class.attribute_names - %w[id]
|
||||
expect(described_class.last(items.size).pluck(*attribute_names)).to eq(
|
||||
items.pluck(*attribute_names))
|
||||
end
|
||||
|
||||
it 'rolls back the transaction when any item is invalid' do
|
||||
# second batch is bad
|
||||
all_items = described_class.valid_list(10) +
|
||||
described_class.invalid_list(10)
|
||||
|
||||
expect do
|
||||
described_class.bulk_insert!(all_items, batch_size: 2) rescue nil
|
||||
end.not_to change { described_class.count }
|
||||
end
|
||||
|
||||
it 'does nothing and returns true when items are empty' do
|
||||
expect(described_class.bulk_insert!([])).to be(true)
|
||||
expect(described_class.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does nothing and returns true when items are empty' do
|
||||
expect(BulkInsertItem.bulk_insert!([])).to be(true)
|
||||
expect(BulkInsertItem.count).to eq(0)
|
||||
context 'when duplicate items are to be inserted' do
|
||||
let!(:existing_object) { described_class.create!(name: 'duplicate', secret_value: 'old value') }
|
||||
let(:new_object) { described_class.new(name: 'duplicate', secret_value: 'new value') }
|
||||
|
||||
describe '.bulk_insert!' do
|
||||
context 'when skip_duplicates is set to false' do
|
||||
it 'raises an exception' do
|
||||
expect { described_class.bulk_insert!([new_object], skip_duplicates: false) }
|
||||
.to raise_error(ActiveRecord::RecordNotUnique)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_duplicates is set to true' do
|
||||
it 'does not update existing object' do
|
||||
described_class.bulk_insert!([new_object], skip_duplicates: true)
|
||||
|
||||
expect(existing_object.reload.secret_value).to eq('old value')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.bulk_upsert!' do
|
||||
it 'updates existing object' do
|
||||
described_class.bulk_upsert!([new_object], unique_by: %w[name])
|
||||
|
||||
expect(existing_object.reload.secret_value).to eq('new value')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1921,30 +1921,15 @@ describe Project do
|
|||
|
||||
describe '#create_repository' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:shell) { Gitlab::Shell.new }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:gitlab_shell).and_return(shell)
|
||||
end
|
||||
|
||||
context 'using a regular repository' do
|
||||
it 'creates the repository' do
|
||||
expect(shell).to receive(:create_repository)
|
||||
.with(project.repository_storage, project.disk_path, project.full_path)
|
||||
.and_return(true)
|
||||
|
||||
expect(project.repository).to receive(:after_create)
|
||||
|
||||
expect(project.repository).to receive(:create_repository)
|
||||
expect(project.create_repository).to eq(true)
|
||||
end
|
||||
|
||||
it 'adds an error if the repository could not be created' do
|
||||
expect(shell).to receive(:create_repository)
|
||||
.with(project.repository_storage, project.disk_path, project.full_path)
|
||||
.and_return(false)
|
||||
|
||||
expect(project.repository).not_to receive(:after_create)
|
||||
|
||||
expect(project.repository).to receive(:create_repository) { raise 'Fail in test' }
|
||||
expect(project.create_repository).to eq(false)
|
||||
expect(project.errors).not_to be_empty
|
||||
end
|
||||
|
@ -1953,7 +1938,7 @@ describe Project do
|
|||
context 'using a forked repository' do
|
||||
it 'does nothing' do
|
||||
expect(project).to receive(:forked?).and_return(true)
|
||||
expect(shell).not_to receive(:create_repository)
|
||||
expect(project.repository).not_to receive(:create_repository)
|
||||
|
||||
project.create_repository
|
||||
end
|
||||
|
@ -1962,28 +1947,16 @@ describe Project do
|
|||
|
||||
describe '#ensure_repository' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:shell) { Gitlab::Shell.new }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:gitlab_shell).and_return(shell)
|
||||
end
|
||||
|
||||
it 'creates the repository if it not exist' do
|
||||
allow(project).to receive(:repository_exists?)
|
||||
.and_return(false)
|
||||
|
||||
allow(shell).to receive(:create_repository)
|
||||
.with(project.repository_storage, project.disk_path, project.full_path)
|
||||
.and_return(true)
|
||||
|
||||
allow(project).to receive(:repository_exists?).and_return(false)
|
||||
expect(project).to receive(:create_repository).with(force: true)
|
||||
|
||||
project.ensure_repository
|
||||
end
|
||||
|
||||
it 'does not create the repository if it exists' do
|
||||
allow(project).to receive(:repository_exists?)
|
||||
.and_return(true)
|
||||
allow(project).to receive(:repository_exists?).and_return(true)
|
||||
|
||||
expect(project).not_to receive(:create_repository)
|
||||
|
||||
|
@ -1992,13 +1965,8 @@ describe Project do
|
|||
|
||||
it 'creates the repository if it is a fork' do
|
||||
expect(project).to receive(:forked?).and_return(true)
|
||||
|
||||
allow(project).to receive(:repository_exists?)
|
||||
.and_return(false)
|
||||
|
||||
expect(shell).to receive(:create_repository)
|
||||
.with(project.repository_storage, project.disk_path, project.full_path)
|
||||
.and_return(true)
|
||||
expect(project).to receive(:repository_exists?).and_return(false)
|
||||
expect(project.repository).to receive(:create_repository) { true }
|
||||
|
||||
project.ensure_repository
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ describe ProjectWiki do
|
|||
|
||||
describe "#url_to_repo" do
|
||||
it "returns the correct ssh url to the repo" do
|
||||
expect(subject.url_to_repo).to eq(gitlab_shell.url_to_repo(subject.full_path))
|
||||
expect(subject.url_to_repo).to eq(Gitlab::Shell.url_to_repo(subject.full_path))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -97,9 +97,7 @@ describe ProjectWiki do
|
|||
it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
|
||||
# Create a fresh project which will not have a wiki
|
||||
project_wiki = described_class.new(create(:project), user)
|
||||
gitlab_shell = double(:gitlab_shell)
|
||||
allow(gitlab_shell).to receive(:create_wiki_repository)
|
||||
allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell)
|
||||
expect(project_wiki.repository).to receive(:create_if_not_exists) { false }
|
||||
|
||||
expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
|
||||
end
|
||||
|
@ -416,26 +414,12 @@ describe ProjectWiki do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#create_repo!' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'creates a repository' do
|
||||
expect(raw_repository.exists?).to eq(false)
|
||||
expect(subject.repository).to receive(:after_create)
|
||||
|
||||
subject.send(:create_repo!, raw_repository)
|
||||
|
||||
expect(raw_repository.exists?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ensure_repository' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'creates the repository if it not exist' do
|
||||
expect(raw_repository.exists?).to eq(false)
|
||||
|
||||
expect(subject).to receive(:create_repo!).and_call_original
|
||||
subject.ensure_repository
|
||||
|
||||
expect(raw_repository.exists?).to eq(true)
|
||||
|
|
|
@ -20,7 +20,6 @@ RSpec.describe Release do
|
|||
|
||||
describe 'validation' do
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
it { is_expected.to validate_presence_of(:description) }
|
||||
it { is_expected.to validate_presence_of(:tag) }
|
||||
|
||||
context 'when a release exists in the database without a name' do
|
||||
|
|
|
@ -262,6 +262,8 @@ describe API::Internal::Base do
|
|||
|
||||
describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do
|
||||
context "access granted" do
|
||||
let(:env) { {} }
|
||||
|
||||
around do |example|
|
||||
Timecop.freeze { example.run }
|
||||
end
|
||||
|
@ -270,30 +272,32 @@ describe API::Internal::Base do
|
|||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'with env passed as a JSON' do
|
||||
let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_container(project) }
|
||||
shared_examples 'sets hook env' do
|
||||
context 'with env passed as a JSON' do
|
||||
let(:obj_dir_relative) { './objects' }
|
||||
let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] }
|
||||
let(:env) do
|
||||
{
|
||||
GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
|
||||
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
|
||||
}
|
||||
end
|
||||
|
||||
it 'sets env in RequestStore' do
|
||||
obj_dir_relative = './objects'
|
||||
alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
|
||||
it 'sets env in RequestStore' do
|
||||
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys)
|
||||
|
||||
expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, {
|
||||
'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
|
||||
'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
|
||||
})
|
||||
subject
|
||||
|
||||
push(key, project.wiki, env: {
|
||||
GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
|
||||
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
|
||||
}.to_json)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "git push with project.wiki" do
|
||||
subject { push(key, project.wiki, env: env.to_json) }
|
||||
|
||||
it 'responds with success' do
|
||||
push(key, project.wiki)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response["status"]).to be_truthy
|
||||
|
@ -301,6 +305,10 @@ describe API::Internal::Base do
|
|||
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
|
||||
expect(user.reload.last_activity_on).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'sets hook env' do
|
||||
let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_container(project) }
|
||||
end
|
||||
end
|
||||
|
||||
context "git pull with project.wiki" do
|
||||
|
@ -328,8 +336,10 @@ describe API::Internal::Base do
|
|||
end
|
||||
|
||||
context 'git push with personal snippet' do
|
||||
subject { push(key, personal_snippet, env: env.to_json) }
|
||||
|
||||
it 'responds with success' do
|
||||
push(key, personal_snippet)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response["status"]).to be_truthy
|
||||
|
@ -338,8 +348,9 @@ describe API::Internal::Base do
|
|||
expect(user.reload.last_activity_on).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'snippets with disabled feature flag' do
|
||||
subject { push(key, personal_snippet) }
|
||||
it_behaves_like 'snippets with disabled feature flag'
|
||||
it_behaves_like 'sets hook env' do
|
||||
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(personal_snippet) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -360,8 +371,10 @@ describe API::Internal::Base do
|
|||
end
|
||||
|
||||
context 'git push with project snippet' do
|
||||
subject { push(key, project_snippet, env: env.to_json) }
|
||||
|
||||
it 'responds with success' do
|
||||
push(key, project_snippet)
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response["status"]).to be_truthy
|
||||
|
@ -370,8 +383,9 @@ describe API::Internal::Base do
|
|||
expect(user.reload.last_activity_on).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'snippets with disabled feature flag' do
|
||||
subject { push(key, project_snippet) }
|
||||
it_behaves_like 'snippets with disabled feature flag'
|
||||
it_behaves_like 'sets hook env' do
|
||||
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(project_snippet) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -406,6 +406,22 @@ describe API::Releases do
|
|||
expect(project.releases.last.description).to eq('Super nice release')
|
||||
end
|
||||
|
||||
it 'creates a new release without description' do
|
||||
params = {
|
||||
name: 'New release without description',
|
||||
tag_name: 'v0.1',
|
||||
released_at: '2019-03-25 10:00:00'
|
||||
}
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/releases", maintainer), params: params
|
||||
end.to change { Release.count }.by(1)
|
||||
|
||||
expect(project.releases.last.name).to eq('New release without description')
|
||||
expect(project.releases.last.tag).to eq('v0.1')
|
||||
expect(project.releases.last.description).to eq(nil)
|
||||
end
|
||||
|
||||
it 'sets the released_at to the current time if the released_at parameter is not provided' do
|
||||
now = Time.zone.parse('2015-08-25 06:00:00Z')
|
||||
Timecop.freeze(now) do
|
||||
|
@ -451,26 +467,6 @@ describe API::Releases do
|
|||
expect(project.releases.last.released_at).to eq('2019-03-25T01:00:00Z')
|
||||
end
|
||||
|
||||
context 'when description is empty' do
|
||||
let(:params) do
|
||||
{
|
||||
name: 'New release',
|
||||
tag_name: 'v0.1',
|
||||
description: ''
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns an error as validation failure' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/releases", maintainer), params: params
|
||||
end.not_to change { Release.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message'])
|
||||
.to eq("Validation failed: Description can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
it 'matches response schema' do
|
||||
post api("/projects/#{project.id}/releases", maintainer), params: params
|
||||
|
||||
|
|
|
@ -119,6 +119,14 @@ describe Ci::UpdateCiRefStatusService do
|
|||
it_behaves_like 'does a noop'
|
||||
end
|
||||
|
||||
context 'pipeline is retried' do
|
||||
before do
|
||||
ci_ref.update!(last_updated_by_pipeline: pipeline)
|
||||
end
|
||||
|
||||
it_behaves_like 'updates ci_ref'
|
||||
end
|
||||
|
||||
context 'ref is stale' do
|
||||
let(:pipeline1) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
|
||||
let(:pipeline2) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
|
||||
|
|
|
@ -312,6 +312,8 @@ describe Projects::ForkService do
|
|||
|
||||
# Stub everything required to move a project to a Gitaly shard that does not exist
|
||||
stub_storage_settings('test_second_storage' => { 'path' => TestEnv::SECOND_STORAGE_PATH })
|
||||
allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow_any_instance_of(Gitlab::Git::Repository).to receive(:replicate)
|
||||
allow_any_instance_of(Gitlab::Git::Repository).to receive(:checksum)
|
||||
.and_return(::Gitlab::Git::BLANK_SHA)
|
||||
|
|
|
@ -32,6 +32,8 @@ describe Projects::UpdateRepositoryStorageService do
|
|||
project.repository.path_to_repo
|
||||
end
|
||||
|
||||
expect(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
expect(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
expect(project_repository_double).to receive(:checksum)
|
||||
|
@ -58,6 +60,8 @@ describe Projects::UpdateRepositoryStorageService do
|
|||
|
||||
context 'when the move fails' do
|
||||
it 'unmarks the repository as read-only without updating the repository storage' do
|
||||
expect(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
expect(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
.and_raise(Gitlab::Git::CommandError)
|
||||
|
@ -73,6 +77,8 @@ describe Projects::UpdateRepositoryStorageService do
|
|||
|
||||
context 'when the checksum does not match' do
|
||||
it 'unmarks the repository as read-only without updating the repository storage' do
|
||||
expect(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
expect(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
expect(project_repository_double).to receive(:checksum)
|
||||
|
@ -91,6 +97,8 @@ describe Projects::UpdateRepositoryStorageService do
|
|||
let!(:pool) { create(:pool_repository, :ready, source_project: project) }
|
||||
|
||||
it 'leaves the pool' do
|
||||
expect(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
expect(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
expect(project_repository_double).to receive(:checksum)
|
||||
|
|
|
@ -44,12 +44,6 @@ describe Releases::UpdateService do
|
|||
it_behaves_like 'a failed update'
|
||||
end
|
||||
|
||||
context 'with an invalid update' do
|
||||
let(:new_description) { '' }
|
||||
|
||||
it_behaves_like 'a failed update'
|
||||
end
|
||||
|
||||
context 'when a milestone is passed in' do
|
||||
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
|
||||
let(:params_with_milestone) { params.merge!({ milestones: [new_title] }) }
|
||||
|
|
|
@ -22,11 +22,15 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
|
|||
|
||||
context 'when the move succeeds', :clean_gitlab_redis_shared_state do
|
||||
before do
|
||||
allow(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
allow(project_repository_double).to receive(:checksum)
|
||||
.and_return(project_repository_checksum)
|
||||
|
||||
allow(repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(repository_double).to receive(:replicate)
|
||||
.with(repository.raw)
|
||||
allow(repository_double).to receive(:checksum)
|
||||
|
@ -90,11 +94,15 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
|
|||
|
||||
context "when the move of the #{repository_type} repository fails" do
|
||||
it 'unmarks the repository as read-only without updating the repository storage' do
|
||||
allow(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
allow(project_repository_double).to receive(:checksum)
|
||||
.and_return(project_repository_checksum)
|
||||
|
||||
allow(repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(repository_double).to receive(:replicate)
|
||||
.with(repository.raw)
|
||||
.and_raise(Gitlab::Git::CommandError)
|
||||
|
@ -111,11 +119,15 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
|
|||
|
||||
context "when the checksum of the #{repository_type} repository does not match" do
|
||||
it 'unmarks the repository as read-only without updating the repository storage' do
|
||||
allow(project_repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(project_repository_double).to receive(:replicate)
|
||||
.with(project.repository.raw)
|
||||
allow(project_repository_double).to receive(:checksum)
|
||||
.and_return(project_repository_checksum)
|
||||
|
||||
allow(repository_double).to receive(:create_repository)
|
||||
.and_return(true)
|
||||
allow(repository_double).to receive(:replicate)
|
||||
.with(repository.raw)
|
||||
allow(repository_double).to receive(:checksum)
|
||||
|
|
|
@ -7766,10 +7766,10 @@ merge2@^1.2.3:
|
|||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
|
||||
integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==
|
||||
|
||||
mermaid@^8.4.5:
|
||||
version "8.4.5"
|
||||
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.5.tgz#48d5722cbc72be2ad01002795835d7ca1b48e000"
|
||||
integrity sha512-oJWgZBtT2rvAdmqHvKjDwb3tOut1+ksfgDdZrVhhNcdzNibzGPjCsmMPpVXjkFYzKZCVunIbAkfxltSuaGIhaw==
|
||||
mermaid@^8.4.8:
|
||||
version "8.4.8"
|
||||
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.8.tgz#8adcfdbc505d6bca52df167cff690427c9727b60"
|
||||
integrity sha512-sumTNBFwMX7oMQgogdr3NhgTeQOiwcEsm23rQ4KHGW7tpmvMwER1S+1gjCSSnqlmM/zw7Ga7oesYCYicKboRwQ==
|
||||
dependencies:
|
||||
"@braintree/sanitize-url" "^3.1.0"
|
||||
crypto-random-string "^3.0.1"
|
||||
|
|
Loading…
Reference in New Issue