Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-16 12:09:12 +00:00
parent 3fd97b4bba
commit cbfe03ae04
86 changed files with 1077 additions and 549 deletions

View File

@ -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.

View File

@ -1,6 +1,5 @@
.run-dev-fixtures:
extends:
- .default-tags
- .default-retry
- .default-cache
- .default-before_script

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,3 @@
.default-tags:
tags:
- gitlab-org
.default-retry:
retry:
max: 2 # This is confusing but this means "3 runs at max".

View File

@ -1,6 +1,5 @@
.only-code-memory-job-base:
extends:
- .default-tags
- .default-retry
- .default-cache
- .default-before_script

View File

@ -1,6 +1,5 @@
pages:
extends:
- .default-tags
- .default-retry
- .default-cache
- .pages:rules

View File

@ -1,6 +1,5 @@
.qa-job-base:
extends:
- .default-tags
- .default-retry
stage: test
needs: []

View File

@ -7,7 +7,6 @@
.rails-job-base:
extends:
- .default-tags
- .default-retry
- .default-cache
- .default-before_script

View File

@ -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

View File

@ -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: []

View File

@ -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

View File

@ -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

View File

@ -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">&times;</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();
}

View File

@ -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

View File

@ -39,6 +39,7 @@ const populateUserInfo = user => {
location: userData.location,
bio: userData.bio,
organization: userData.organization,
jobTitle: userData.job_title,
loaded: true,
});
}

View File

@ -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>

View File

@ -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"
/>

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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) }

View File

@ -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)

View File

@ -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

View File

@ -99,7 +99,7 @@
%p
GitLab Shell
%span.float-right
= Gitlab::Shell.new.version
= Gitlab::Shell.version
%p
GitLab Workhorse
%span.float-right

View File

@ -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')
&nbsp;
= link_to '#', class: 'btn btn-default' do
= icon('rss')
%h2#lists Lists
.lead

View File

@ -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

View File

@ -0,0 +1,2 @@
.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
= yield

View File

@ -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)

View File

@ -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?

View File

@ -0,0 +1,5 @@
---
title: Add CI template to deploy to ECS
merge_request: 26371
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Optimize Project related count with slack service
merge_request: 26686
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Correctly send notification on pipeline retry
merge_request: 26803
author: Jacopo Beschi @jacopo-beschi
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add "Job Title" field in user settings and display on profile
merge_request: 25155
author:
type: added

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fix bug setting hook env with personal snippets
merge_request: 27235
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add functionality to render individual mermaids
merge_request: 26564
author:
type: changed

View File

@ -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

View File

@ -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."

View File

@ -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

View File

@ -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

View File

@ -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)"

View File

@ -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. |

View File

@ -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

View File

@ -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!

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -50,7 +50,7 @@ module SystemCheck
end
def gitlab_shell_version
Gitlab::Shell.new.version
Gitlab::Shell.version
end
end
end

View File

@ -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|

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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)

View File

@ -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)

View File

@ -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] }) }

View File

@ -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)

View File

@ -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"