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-01-28 12:09:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-28 12:09:06 +0300
commit7e8278c0f46cf6058efad5afd0aef177977bd663 (patch)
tree7ac46710921145bb782bcb208ea896e1548b168b /app
parentbbf6581214128ae12a6ff32f66a0d03ee57a2e91 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js47
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js36
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js1
-rw-r--r--app/assets/javascripts/repository/utils/title.js14
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/carousel.scss202
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/services/groups/import_export/import_service.rb61
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/group_export_worker.rb4
-rw-r--r--app/workers/group_import_worker.rb15
11 files changed, 377 insertions, 7 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
index 052e33b4a2b..d5d8edd5ac0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
@@ -1,26 +1,67 @@
import Mousetrap from 'mousetrap';
-import { getLocationHash, visitUrl } from '../../lib/utils/url_utility';
+import {
+ getLocationHash,
+ updateHistory,
+ urlIsDifferent,
+ urlContainsSha,
+ getShaFromUrl,
+} from '~/lib/utils/url_utility';
+import { updateRefPortionOfTitle } from '~/repository/utils/title';
import Shortcuts from './shortcuts';
const defaults = {
skipResetBindings: false,
fileBlobPermalinkUrl: null,
+ fileBlobPermalinkUrlElement: null,
};
+function eventHasModifierKeys(event) {
+ // We ignore alt because I don't think alt clicks normally do anything special?
+ return event.ctrlKey || event.metaKey || event.shiftKey;
+}
+
export default class ShortcutsBlob extends Shortcuts {
constructor(opts) {
const options = Object.assign({}, defaults, opts);
super(options.skipResetBindings);
this.options = options;
+ this.shortcircuitPermalinkButton();
+
Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
}
moveToFilePermalink() {
- if (this.options.fileBlobPermalinkUrl) {
+ const permalink = this.options.fileBlobPermalinkUrl;
+
+ if (permalink) {
const hash = getLocationHash();
const hashUrlString = hash ? `#${hash}` : '';
- visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+
+ if (urlIsDifferent(permalink)) {
+ updateHistory({
+ url: `${permalink}${hashUrlString}`,
+ title: document.title,
+ });
+ }
+
+ if (urlContainsSha({ url: permalink })) {
+ updateRefPortionOfTitle(getShaFromUrl({ url: permalink }));
+ }
+ }
+ }
+
+ shortcircuitPermalinkButton() {
+ const button = this.options.fileBlobPermalinkUrlElement;
+ const handleButton = e => {
+ if (!eventHasModifierKeys(e)) {
+ e.preventDefault();
+ this.moveToFilePermalink();
+ }
+ };
+
+ if (button) {
+ button.addEventListener('click', handleButton);
}
}
}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index d48678c21f6..202363a1dda 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,6 +1,14 @@
const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
+const SHA_REGEX = /[\da-f]{40}/gi;
+
+// Reset the cursor in a Regex so that multiple uses before a recompile don't fail
+function resetRegExp(regex) {
+ regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */
+
+ return regex;
+}
// Returns a decoded url parameter value
// - Treats '+' as '%20'
@@ -128,6 +136,20 @@ export function doesHashExistInUrl(hashName) {
return hash && hash.includes(hashName);
}
+export function urlContainsSha({ url = String(window.location) } = {}) {
+ return resetRegExp(SHA_REGEX).test(url);
+}
+
+export function getShaFromUrl({ url = String(window.location) } = {}) {
+ let sha = null;
+
+ if (urlContainsSha({ url })) {
+ [sha] = url.match(resetRegExp(SHA_REGEX));
+ }
+
+ return sha;
+}
+
/**
* Apply the fragment to the given url by returning a new url string that includes
* the fragment. If the given url already contains a fragment, the original fragment
@@ -154,6 +176,16 @@ export function visitUrl(url, external = false) {
}
}
+export function updateHistory({ state = {}, title = '', url, replace = false, win = window } = {}) {
+ if (win.history) {
+ if (replace) {
+ win.history.replaceState(state, title, url);
+ } else {
+ win.history.pushState(state, title, url);
+ }
+ }
+}
+
export function refreshCurrentPage() {
visitUrl(window.location.href);
}
@@ -282,3 +314,7 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
};
export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+
+export function urlIsDifferent(url, compare = String(window.location)) {
+ return url !== compare;
+}
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index bd8afa2d5ba..e862456f429 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -25,6 +25,7 @@ export default () => {
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
+ fileBlobPermalinkUrlElement,
});
new BlobForkSuggestion({
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
index ff16fbdd420..9c4b334a1ce 100644
--- a/app/assets/javascripts/repository/utils/title.js
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -1,5 +1,5 @@
const DEFAULT_TITLE = '· GitLab';
-// eslint-disable-next-line import/prefer-default-export
+
export const setTitle = (pathMatch, ref, project) => {
if (!pathMatch) {
document.title = `${project} ${DEFAULT_TITLE}`;
@@ -12,3 +12,15 @@ export const setTitle = (pathMatch, ref, project) => {
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project} ${DEFAULT_TITLE}`;
};
+
+export function updateRefPortionOfTitle(sha, doc = document) {
+ const { title = '' } = doc;
+ const titleParts = title.split(' · ');
+
+ if (titleParts.length > 1) {
+ titleParts[1] = sha;
+
+ /* eslint-disable-next-line no-param-reassign */
+ doc.title = titleParts.join(' · ');
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 249e9a24b17..9032dd28b80 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -15,6 +15,7 @@
@import 'framework/badges';
@import 'framework/calendar';
@import 'framework/callout';
+@import 'framework/carousel';
@import 'framework/common';
@import 'framework/dropdowns';
@import 'framework/files';
diff --git a/app/assets/stylesheets/framework/carousel.scss b/app/assets/stylesheets/framework/carousel.scss
new file mode 100644
index 00000000000..d51a9f9c173
--- /dev/null
+++ b/app/assets/stylesheets/framework/carousel.scss
@@ -0,0 +1,202 @@
+// Notes on the classes:
+//
+// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)
+// even when their scroll action started on a carousel, but for compatibility (with Firefox)
+// we're preventing all actions instead
+// 2. The .carousel-item-left and .carousel-item-right is used to indicate where
+// the active slide is heading.
+// 3. .active.carousel-item is the current slide.
+// 4. .active.carousel-item-left and .active.carousel-item-right is the current
+// slide in its in-transition state. Only one of these occurs at a time.
+// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right
+// is the upcoming slide in transition.
+
+.carousel {
+ position: relative;
+
+ &.pointer-event {
+ touch-action: pan-y;
+ }
+}
+
+
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ @include clearfix();
+}
+
+.carousel-item {
+ position: relative;
+ display: none;
+ float: left;
+ width: 100%;
+ margin-right: -100%;
+ backface-visibility: hidden;
+ @include transition($carousel-transition);
+}
+
+.carousel-item.active,
+.carousel-item-next,
+.carousel-item-prev {
+ display: block;
+}
+
+.carousel-item-next:not(.carousel-item-left),
+.active.carousel-item-right {
+ transform: translateX(100%);
+}
+
+.carousel-item-prev:not(.carousel-item-right),
+.active.carousel-item-left {
+ transform: translateX(-100%);
+}
+
+
+//
+// Alternate transitions
+//
+
+.carousel-fade {
+ .carousel-item {
+ opacity: 0;
+ transition-property: opacity;
+ transform: none;
+ }
+
+ .carousel-item.active,
+ .carousel-item-next.carousel-item-left,
+ .carousel-item-prev.carousel-item-right {
+ z-index: 1;
+ opacity: 1;
+ }
+
+ .active.carousel-item-left,
+ .active.carousel-item-right {
+ z-index: 0;
+ opacity: 0;
+ @include transition(0s $carousel-transition-duration opacity);
+ }
+}
+
+
+//
+// Left/right controls for nav
+//
+
+.carousel-control-prev,
+.carousel-control-next {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ // Use flex for alignment (1-3)
+ display: flex; // 1. allow flex styles
+ align-items: center; // 2. vertically center contents
+ justify-content: center; // 3. horizontally center contents
+ width: $carousel-control-width;
+ color: $carousel-control-color;
+ text-align: center;
+ opacity: $carousel-control-opacity;
+ @include transition($carousel-control-transition);
+
+ // Hover/focus state
+ @include hover-focus {
+ color: $carousel-control-color;
+ text-decoration: none;
+ outline: 0;
+ opacity: $carousel-control-hover-opacity;
+ }
+}
+
+.carousel-control-prev {
+ left: 0;
+ @if $enable-gradients {
+ background: linear-gradient(90deg, rgba($black, 0.25), rgba($black, 0.001));
+ }
+}
+
+.carousel-control-next {
+ right: 0;
+ @if $enable-gradients {
+ background: linear-gradient(270deg, rgba($black, 0.25), rgba($black, 0.001));
+ }
+}
+
+// Icons for within
+.carousel-control-prev-icon,
+.carousel-control-next-icon {
+ display: inline-block;
+ width: $carousel-control-icon-width;
+ height: $carousel-control-icon-width;
+ background: no-repeat 50% / 100% 100%;
+}
+
+.carousel-control-prev-icon {
+ background-image: $carousel-control-prev-icon-bg;
+}
+
+.carousel-control-next-icon {
+ background-image: $carousel-control-next-icon-bg;
+}
+
+
+// Optional indicator pips
+//
+// Add an ordered list with the following class and add a list item for each
+// slide your carousel holds.
+
+.carousel-indicators {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 15;
+ display: flex;
+ justify-content: center;
+ padding-left: 0; // override <ol> default
+ // Use the .carousel-control's width as margin so we don't overlay those
+ margin-right: $carousel-control-width;
+ margin-left: $carousel-control-width;
+ list-style: none;
+
+ li {
+ box-sizing: content-box;
+ flex: 0 1 auto;
+ width: $carousel-indicator-width;
+ height: $carousel-indicator-height;
+ margin-right: $carousel-indicator-spacer;
+ margin-left: $carousel-indicator-spacer;
+ text-indent: -999px;
+ cursor: pointer;
+ background-color: $carousel-indicator-active-bg;
+ background-clip: padding-box;
+ // Use transparent borders to increase the hit area by 10px on top and bottom.
+ border-top: $carousel-indicator-hit-area-height solid transparent;
+ border-bottom: $carousel-indicator-hit-area-height solid transparent;
+ opacity: 0.5;
+ @include transition($carousel-indicator-transition);
+ }
+
+ .active {
+ opacity: 1;
+ }
+}
+
+
+// Optional captions
+//
+//
+
+.carousel-caption {
+ position: absolute;
+ right: (100% - $carousel-caption-width) / 2;
+ bottom: 20px;
+ left: (100% - $carousel-caption-width) / 2;
+ z-index: 10;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ color: $carousel-caption-color;
+ text-align: center;
+}
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 7e3ba98d86c..2bf0ee57d39 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -578,7 +578,7 @@ module Ci
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
- # them using the +Gitlab::ImportExport::RelationFactory+ class.
+ # them using the +Gitlab::ImportExport::ProjectRelationFactory+ class.
def notes=(notes)
notes.each do |note|
note[:id] = nil
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
new file mode 100644
index 00000000000..628c8f5bac0
--- /dev/null
+++ b/app/services/groups/import_export/import_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Groups
+ module ImportExport
+ class ImportService
+ attr_reader :current_user, :group, :params
+
+ def initialize(group:, user:)
+ @group = group
+ @current_user = user
+ @shared = Gitlab::ImportExport::Shared.new(@group)
+ end
+
+ def execute
+ validate_user_permissions
+
+ if import_file && restorer.restore
+ @group
+ else
+ raise StandardError.new(@shared.errors.to_sentence)
+ end
+ rescue => e
+ raise StandardError.new(e.message)
+ ensure
+ remove_import_file
+ end
+
+ private
+
+ def import_file
+ @import_file ||= Gitlab::ImportExport::FileImporter.import(importable: @group,
+ archive_file: nil,
+ shared: @shared)
+ end
+
+ def restorer
+ @restorer ||= Gitlab::ImportExport::GroupTreeRestorer.new(user: @current_user,
+ shared: @shared,
+ group: @group,
+ group_hash: nil)
+ end
+
+ def remove_import_file
+ upload = @group.import_export_upload
+
+ return unless upload&.import_file&.file
+
+ upload.remove_import_file!
+ upload.save!
+ end
+
+ def validate_user_permissions
+ unless current_user.can?(:admin_group, group)
+ raise ::Gitlab::ImportExport::Error.new(
+ "User with ID: %s does not have permission to Group %s with ID: %s." %
+ [current_user.id, group.name, group.id])
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 87feecf4bbb..1e7b5a1cf65 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -137,6 +137,7 @@
- gitlab_shell
- group_destroy
- group_export
+- group_import
- import_issues_csv
- invalid_gpg_signature_update
- irker
diff --git a/app/workers/group_export_worker.rb b/app/workers/group_export_worker.rb
index 51dbdc95661..a2d34e8c8bf 100644
--- a/app/workers/group_export_worker.rb
+++ b/app/workers/group_export_worker.rb
@@ -4,11 +4,11 @@ class GroupExportWorker
include ApplicationWorker
include ExceptionBacktrace
- feature_category :source_code_management
+ feature_category :importers
def perform(current_user_id, group_id, params = {})
current_user = User.find(current_user_id)
- group = Group.find(group_id)
+ group = Group.find(group_id)
::Groups::ImportExport::ExportService.new(group: group, user: current_user, params: params).execute
end
diff --git a/app/workers/group_import_worker.rb b/app/workers/group_import_worker.rb
new file mode 100644
index 00000000000..f283eab5814
--- /dev/null
+++ b/app/workers/group_import_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class GroupImportWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ feature_category :importers
+
+ def perform(user_id, group_id)
+ current_user = User.find(user_id)
+ group = Group.find(group_id)
+
+ ::Groups::ImportExport::ImportService.new(group: group, user: current_user).execute
+ end
+end