Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-16 15:09:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-16 15:09:12 +0300
commitcbfe03ae04a52d9825ff7cbeccdfe5d313adf6a2 (patch)
treee4879b35d019d3bbba1689f3ac4c48b81bf7b451 /app
parent3fd97b4bba24ca412112aad025a38a32c7a6cf8c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js153
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue54
-rw-r--r--app/assets/stylesheets/framework/blocks.scss14
-rw-r--r--app/assets/stylesheets/framework/mixins.scss18
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
-rw-r--r--app/controllers/projects/tags/releases_controller.rb6
-rw-r--r--app/helpers/submodule_helper.rb2
-rw-r--r--app/helpers/users_helper.rb15
-rw-r--r--app/models/concerns/bulk_insert_safe.rb62
-rw-r--r--app/models/concerns/has_repository.rb3
-rw-r--r--app/models/project.rb15
-rw-r--r--app/models/project_wiki.rb25
-rw-r--r--app/models/release.rb2
-rw-r--r--app/services/ci/update_ci_ref_status_service.rb2
-rw-r--r--app/services/projects/update_repository_storage_service.rb13
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/help/ui.html.haml15
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/users/_cover_controls.html.haml2
-rw-r--r--app/views/users/_profile_basic_info.html.haml2
-rw-r--r--app/views/users/show.html.haml47
24 files changed, 321 insertions, 155 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index b5e17a0587d..fe63ebd470d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -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();
}
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index bfb760f3579..c93a95e490a 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -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
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 5cc22f62262..f8c1c3634c2 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -39,6 +39,7 @@ const populateUserInfo = user => {
location: userData.location,
bio: userData.bio,
organization: userData.organization,
+ jobTitle: userData.job_title,
loaded: true,
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index b85be8b9652..c38272ab239 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index ca25d9ee738..602d4ab89e1 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -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"
/>
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0e4080ce201..f922d8bcaab 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -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;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index fd448ee24ed..621a4eddc34 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 43cf0d4bd70..82b3698287c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -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;
diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb
index 5e4c601a693..c1f4cbce054 100644
--- a/app/controllers/projects/tags/releases_controller.rb
+++ b/app/controllers/projects/tags/releases_controller.rb
@@ -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
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 32c613ab4ad..1149b168383 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -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)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index e87bb27cf62..c1bca6b4c41 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -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
diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb
index a4814fc0d48..a61db2dc148 100644
--- a/app/models/concerns/bulk_insert_safe.rb
+++ b/app/models/concerns/bulk_insert_safe.rb
@@ -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?
-
- _bulk_insert_in_batches(items, batch_size, validate, &handle_attributes)
+ 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
- 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)
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 0887236e65e..cc792eab2e0 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 6d9a0e5e813..4017fe31b84 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 8ed7811b468..f8528a41634 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -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)
diff --git a/app/models/release.rb b/app/models/release.rb
index 2543717895f..45c2a56d764 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -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) }
diff --git a/app/services/ci/update_ci_ref_status_service.rb b/app/services/ci/update_ci_ref_status_service.rb
index e5e5b94b629..4f7ac4d11b0 100644
--- a/app/services/ci/update_ci_ref_status_service.rb
+++ b/app/services/ci/update_ci_ref_status_service.rb
@@ -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)
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 0adfd4f8fd7..0602089a3ab 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -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
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index f5f6175d3d8..68f761c75d8 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -99,7 +99,7 @@
%p
GitLab Shell
%span.float-right
- = Gitlab::Shell.new.version
+ = Gitlab::Shell.version
%p
GitLab Workhorse
%span.float-right
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 038befac420..d71650ae50c 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -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
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 49533c18c8f..86e157ee042 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -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
diff --git a/app/views/users/_cover_controls.html.haml b/app/views/users/_cover_controls.html.haml
new file mode 100644
index 00000000000..43278e9d232
--- /dev/null
+++ b/app/views/users/_cover_controls.html.haml
@@ -0,0 +1,2 @@
+.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
+ = yield
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index af0a766bab0..c431a72d0e7 100644
--- a/app/views/users/_profile_basic_info.html.haml
+++ b/app/views/users/_profile_basic_info.html.haml
@@ -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)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 3c164588b13..9f5124afc16 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -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?