summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dispatcher.js26
-rw-r--r--app/assets/javascripts/gl_field_error.js5
-rw-r--r--app/assets/javascripts/gl_field_errors.js36
-rw-r--r--app/assets/javascripts/gl_form.js165
-rw-r--r--app/assets/javascripts/labels_select.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js2
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/notes.js7
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js3
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js6
-rw-r--r--app/assets/javascripts/users/index.js8
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue3
-rw-r--r--app/assets/stylesheets/framework/new-sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/controllers/concerns/preview_markdown.rb22
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/projects/wikis_controller.rb13
-rw-r--r--app/controllers/projects_controller.rb13
-rw-r--r--app/controllers/snippets_controller.rb12
-rw-r--r--app/helpers/issuables_helper.rb12
-rw-r--r--app/models/blob.rb4
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/services/merge_requests/merge_service.rb18
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/views/groups/milestones/_form.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml10
-rw-r--r--app/views/notify/pipeline_success_email.html.haml10
-rw-r--r--app/views/projects/new.html.haml94
-rw-r--r--app/views/users/show.html.haml3
-rw-r--r--changelogs/unreleased/34897-delete-branch-after-merge.yml5
-rw-r--r--changelogs/unreleased/35580-cannot-import-project-with-milestones.yml5
-rw-r--r--changelogs/unreleased/35652-prometheus-service-page-shows-error.yml5
-rw-r--r--changelogs/unreleased/37660-match-sidebar-colors.yml5
-rw-r--r--changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml5
-rw-r--r--changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml5
-rw-r--r--changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml6
-rw-r--r--changelogs/unreleased/cache-issuable-template-names.yml5
-rw-r--r--changelogs/unreleased/issue-36484.yml5
-rw-r--r--changelogs/unreleased/move_markdown_preview_to_concern.yml5
-rw-r--r--changelogs/unreleased/replace_explore_projects-feature.yml5
-rw-r--r--config/routes/group.rb4
-rw-r--r--config/webpack.config.js3
-rw-r--r--doc/development/README.md6
-rw-r--r--doc/development/i18n/translation.md12
-rw-r--r--doc/development/testing_guide/index.md1
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/integration/google.md138
-rw-r--r--doc/update/10.0-to-10.1.md6
-rw-r--r--doc/user/discussions/index.md3
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/clusters/index.md90
-rw-r--r--doc/user/project/index.md2
-rw-r--r--features/explore/projects.feature144
-rw-r--r--features/steps/explore/projects.rb145
-rw-r--r--features/steps/shared/paths.rb13
-rw-r--r--features/steps/shared/project.rb18
-rw-r--r--lib/api/api_guard.rb133
-rw-r--r--lib/api/helpers.rb52
-rw-r--r--lib/gitlab/file_detector.rb28
-rw-r--r--lib/gitlab/git/repository.rb6
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb51
-rw-r--r--lib/gitlab/project_template.rb6
-rw-r--r--spec/factories/merge_requests.rb6
-rw-r--r--spec/features/dashboard/groups_list_spec.rb20
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb72
-rw-r--r--spec/features/groups/milestone_spec.rb21
-rw-r--r--spec/features/projects/issues/list_spec.rb20
-rw-r--r--spec/features/projects/issues/user_views_issues_spec.rb56
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb143
-rw-r--r--spec/features/projects/user_views_details_spec.rb151
-rw-r--r--spec/javascripts/awards_handler_spec.js5
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js5
-rw-r--r--spec/javascripts/gl_field_errors_spec.js180
-rw-r--r--spec/javascripts/gl_form_spec.js9
-rw-r--r--spec/javascripts/merge_request_notes_spec.js14
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js5
-rw-r--r--spec/javascripts/notes_spec.js27
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js12
-rw-r--r--spec/javascripts/search_autocomplete_spec.js30
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb12
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/project.group.json188
-rw-r--r--spec/lib/gitlab/import_export/project.light.json51
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb134
-rw-r--r--spec/models/repository_spec.rb4
-rw-r--r--spec/requests/api/helpers_spec.rb18
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb6
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb23
-rw-r--r--spec/services/notification_service_spec.rb12
-rw-r--r--spec/support/api/scopes/read_user_shared_examples.rb8
-rw-r--r--spec/support/email_helpers.rb14
-rw-r--r--spec/support/project_forks_helper.rb2
-rw-r--r--vendor/gitignore/Android.gitignore3
-rw-r--r--vendor/gitignore/Autotools.gitignore9
-rw-r--r--vendor/gitignore/Elixir.gitignore2
-rw-r--r--vendor/gitignore/ExtJs.gitignore2
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore2
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore18
-rw-r--r--vendor/gitignore/Global/macOS.gitignore2
-rw-r--r--vendor/gitignore/Joomla.gitignore2
-rw-r--r--vendor/gitignore/OCaml.gitignore3
-rw-r--r--vendor/gitignore/Python.gitignore5
-rw-r--r--vendor/gitignore/Qt.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore1
-rw-r--r--vendor/gitignore/Terraform.gitignore3
-rw-r--r--vendor/gitignore/Umbraco.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore6
-rw-r--r--vendor/gitignore/ZendFramework.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/Python.gitlab-ci.yml32
115 files changed, 1699 insertions, 1110 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 5ebc92110dd..0a653d7fefc 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -80,6 +80,8 @@ import initChangesDropdown from './init_changes_dropdown';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
+import GlFieldErrors from './gl_field_errors';
+import GLForm from './gl_form';
import U2FAuthenticate from './u2f/authenticate';
(function() {
@@ -231,7 +233,7 @@ import U2FAuthenticate from './u2f/authenticate';
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
- new gl.GLForm($('.milestone-form'), true);
+ new GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
new gl.Diff();
@@ -248,7 +250,7 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.issue-form'), true);
+ new GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -272,7 +274,7 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.merge-request-form'), true);
+ new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -281,7 +283,7 @@ import U2FAuthenticate from './u2f/authenticate';
break;
case 'projects:tags:new':
new ZenMode();
- new gl.GLForm($('.tag-form'), true);
+ new GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'));
break;
case 'projects:snippets:show':
@@ -291,17 +293,17 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
- new gl.GLForm($('.snippet-form'), true);
+ new GLForm($('.snippet-form'), true);
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
- new gl.GLForm($('.snippet-form'), false);
+ new GLForm($('.snippet-form'), false);
break;
case 'projects:releases:edit':
new ZenMode();
- new gl.GLForm($('.release-form'), true);
+ new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
@@ -607,7 +609,7 @@ import U2FAuthenticate from './u2f/authenticate';
new Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
- new gl.GLForm($('.wiki-form'), true);
+ new GLForm($('.wiki-form'), true);
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
@@ -632,12 +634,6 @@ import U2FAuthenticate from './u2f/authenticate';
shortcut_handler = new ShortcutsNavigation();
}
break;
- case 'users':
- const action = path[1];
- import(/* webpackChunkName: 'user_profile' */ './users')
- .then(user => user.default(action))
- .catch(() => {});
- break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
@@ -658,7 +654,7 @@ import U2FAuthenticate from './u2f/authenticate';
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
- new gl.GlFieldErrors(form);
+ new GlFieldErrors(form);
});
};
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 0add7075254..bd63f6f16f0 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline';
const errorAnchorSelector = '.gl-field-error-anchor';
const ignoreInputSelector = '.gl-field-error-ignore';
-class GlFieldError {
+export default class GlFieldError {
constructor({ input, formErrors }) {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
@@ -159,6 +159,3 @@ class GlFieldError {
this.fieldErrorElement.hide();
}
}
-
-window.gl = window.gl || {};
-window.gl.GlFieldError = GlFieldError;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 4bef60264bb..73bcbd93565 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -1,42 +1,40 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-
-import './gl_field_error';
+import GlFieldError from './gl_field_error';
const customValidationFlag = 'gl-field-error-ignore';
-class GlFieldErrors {
+export default class GlFieldErrors {
constructor(form) {
this.form = $(form);
this.state = {
inputs: [],
- valid: false
+ valid: false,
};
this.initValidators();
}
- initValidators () {
+ initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
- .map((selector) => `input${selector}`).join(',');
+ .map(selector => `input${selector}`).join(',');
this.state.inputs = this.form.find(validateSelectors).toArray()
- .filter((input) => !input.classList.contains(customValidationFlag))
- .map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
+ .filter(input => !input.classList.contains(customValidationFlag))
+ .map(input => new GlFieldError({ input, formErrors: this }));
- this.form.on('submit', this.catchInvalidFormSubmit);
+ this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
/* Neccessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
- catchInvalidFormSubmit (event) {
- const $form = $(event.currentTarget);
+ static catchInvalidFormSubmit(e) {
+ const $form = $(e.currentTarget);
if (!$form.attr('novalidate')) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
+ if (!e.currentTarget.checkValidity()) {
+ e.preventDefault();
+ e.stopPropagation();
}
}
}
@@ -50,11 +48,9 @@ class GlFieldErrors {
});
}
- focusOnFirstInvalid () {
- const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ focusOnFirstInvalid() {
+ const firstInvalid = this.state.inputs
+ .filter(input => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus();
}
}
-
-window.gl = window.gl || {};
-window.gl.GlFieldErrors = GlFieldErrors;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 4e8141b2956..48d0c12143a 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,104 +1,99 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
-/* global GitLab */
/* global DropzoneInput */
/* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
-window.gl = window.gl || {};
-
-function GLForm(form, enableGFM = false) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = enableGFM;
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
-}
-
-GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- if (this.autoComplete) {
- this.autoComplete.destroy();
+export default class GLForm {
+ constructor(form, enableGFM = false) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ this.enableGFM = enableGFM;
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
}
- return this.form.data('gl-form', null);
-};
-GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
- this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- this.autoComplete.setup(this.form.find('.js-gfm-input'), {
- emojis: true,
- members: this.enableGFM,
- issues: this.enableGFM,
- milestones: this.enableGFM,
- mergeRequests: this.enableGFM,
- labels: this.enableGFM,
- });
- new DropzoneInput(this.form);
- autosize(this.textarea);
+ destroy() {
+ // Clean form listeners
+ this.clearEventListeners();
+ if (this.autoComplete) {
+ this.autoComplete.destroy();
+ }
+ this.form.data('gl-form', null);
}
- // form and textarea event listeners
- this.addEventListeners();
- gl.text.init(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
- this.form.show();
- if (this.isAutosizeable) this.setupAutosize();
-};
-GLForm.prototype.setupAutosize = function () {
- this.textarea.off('autosize:resized')
- .on('autosize:resized', this.setHeightData.bind(this));
+ setupForm() {
+ const isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
+ this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ this.autoComplete.setup(this.form.find('.js-gfm-input'), {
+ emojis: true,
+ members: this.enableGFM,
+ issues: this.enableGFM,
+ milestones: this.enableGFM,
+ mergeRequests: this.enableGFM,
+ labels: this.enableGFM,
+ });
+ new DropzoneInput(this.form); // eslint-disable-line no-new
+ autosize(this.textarea);
+ }
+ // form and textarea event listeners
+ this.addEventListeners();
+ gl.text.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+ }
- this.textarea.off('mouseup.autosize')
- .on('mouseup.autosize', this.destroyAutosize.bind(this));
+ setupAutosize() {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
- setTimeout(() => {
- autosize(this.textarea);
- this.textarea.css('resize', 'vertical');
- }, 0);
-};
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
-GLForm.prototype.setHeightData = function () {
- this.textarea.data('height', this.textarea.outerHeight());
-};
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+ }
-GLForm.prototype.destroyAutosize = function () {
- const outerHeight = this.textarea.outerHeight();
+ setHeightData() {
+ this.textarea.data('height', this.textarea.outerHeight());
+ }
- if (this.textarea.data('height') === outerHeight) return;
+ destroyAutosize() {
+ const outerHeight = this.textarea.outerHeight();
- autosize.destroy(this.textarea);
+ if (this.textarea.data('height') === outerHeight) return;
- this.textarea.data('height', outerHeight);
- this.textarea.outerHeight(outerHeight);
- this.textarea.css('max-height', window.outerHeight);
-};
+ autosize.destroy(this.textarea);
-GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
-};
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+ }
-GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
-};
+ clearEventListeners() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ gl.text.removeListeners(this.form);
+ }
-window.gl.GLForm = GLForm;
+ addEventListeners() {
+ this.textarea.on('focus', function focusTextArea() {
+ $(this).closest('.md-area').addClass('is-focused');
+ });
+ this.textarea.on('blur', function blurTextArea() {
+ $(this).closest('.md-area').removeClass('is-focused');
+ });
+ }
+}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d479f7ed682..84602cf9207 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -285,7 +285,7 @@ import CreateLabelDropdown from './create_label';
},
hidden: function() {
var isIssueIndex, isMRIndex, page, selectedLabels;
- page = $('body').data('page');
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
@@ -325,7 +325,7 @@ import CreateLabelDropdown from './create_label';
$loading.fadeOut();
};
- page = $('body').data('page');
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 423a25fbdfa..9f05cf16967 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,5 +1,5 @@
-export const getPagePath = (index = 0) => $('body').data('page').split(':')[index];
+export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
export const isInGroupsPage = () => getPagePath() === 'groups';
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 4675b1fcb8f..951d5e559b4 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -147,7 +147,7 @@ import _ from 'underscore';
const { $el, e } = options;
let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
- page = $('body').data('page');
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index cf7322ba1da..790f78d2e11 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -19,6 +19,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
+import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
@@ -557,7 +558,7 @@ export default class Notes {
*/
setupNoteForm(form) {
var textarea, key;
- new gl.GLForm(form, this.enableGFM);
+ this.glForm = new GLForm(form, this.enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
@@ -1152,7 +1153,7 @@ export default class Notes {
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
- new gl.GLForm($editForm.find('form'), this.enableGFM);
+ this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
.attr('action', postUrl)
@@ -1257,7 +1258,7 @@ export default class Notes {
}
static checkMergeRequestStatus() {
- if (getPagePath(1) === 'merge_requests') {
+ if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {
gl.mrWidget.checkStatus();
}
}
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
index 50c725aa3d5..f1cf6e92ef5 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '../vue_shared/translate';
+import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
@@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
- gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
+ gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
});
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index a4d50a52315..55c93923cc8 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -81,7 +81,11 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => {
- $.getJSON(this.activeMetricsEndpoint)
+ $.ajax({
+ url: this.activeMetricsEndpoint,
+ dataType: 'json',
+ global: false,
+ })
.done((res) => {
if (res && res.success) {
stop(res);
diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js
index 33a83f8dae5..9fd8452a2b6 100644
--- a/app/assets/javascripts/users/index.js
+++ b/app/assets/javascripts/users/index.js
@@ -1,7 +1,7 @@
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
-export default function initUserProfile(action) {
+function initUserProfile(action) {
// place profile avatars to top
$('.profile-groups-avatars').tooltip({
placement: 'top',
@@ -17,3 +17,9 @@ export default function initUserProfile(action) {
$(this).parents('.project-limit-message').remove();
});
}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const page = $('body').attr('data-page');
+ const action = page.split(':')[1];
+ initUserProfile(action);
+});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 73676bd6de7..a0883b32593 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -424,7 +424,7 @@ function UsersSelect(currentUser, els) {
}
var isIssueIndex, isMRIndex, page, selected;
- page = $('body').data('page');
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index af4187fab46..8c0d9b9cda8 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import Flash from '../../../flash';
+ import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
@@ -85,7 +86,7 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new gl.GLForm($(this.$refs['gl-form']), true);
+ return new GLForm($(this.$refs['gl-form']), true);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss
index caf4c7a40b1..78972717932 100644
--- a/app/assets/stylesheets/framework/new-sidebar.scss
+++ b/app/assets/stylesheets/framework/new-sidebar.scss
@@ -90,7 +90,7 @@ $new-sidebar-collapsed-width: 50px;
top: $header-height;
bottom: 0;
left: 0;
- background-color: $gray-normal;
+ background-color: $gray-light;
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 5c96b3b78e7..3fd2549b143 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -6,6 +6,7 @@
margin: 0;
list-style: none;
height: auto;
+ border-bottom: 1px solid $border-color;
li {
display: flex;
@@ -24,6 +25,7 @@
&:focus {
text-decoration: none;
color: $black;
+ border-bottom: 2px solid $gray-darkest;
.badge {
color: $black;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 61dc9f13d50..bd385db9692 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -634,10 +634,14 @@ a.deploy-project-label {
}
.project-import {
- .form-group {
+ .import-btn-container {
margin-bottom: 0;
}
+ .toggle-import-form {
+ padding-bottom: 10px;
+ }
+
.import-buttons {
padding-left: 0;
display: -webkit-flex;
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
new file mode 100644
index 00000000000..5ce602b55a8
--- /dev/null
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -0,0 +1,22 @@
+module PreviewMarkdown
+ extend ActiveSupport::Concern
+
+ def preview_markdown
+ result = PreviewMarkdownService.new(@project, current_user, params).execute
+
+ markdown_params =
+ case controller_name
+ when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
+ when 'snippets' then { skip_project_check: true }
+ else {}
+ end
+
+ render json: {
+ body: view_context.markdown(result[:text], markdown_params),
+ references: {
+ users: result[:users],
+ commands: view_context.markdown(result[:commands])
+ }
+ }
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3769a2cde33..a962d82e3b5 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -2,6 +2,7 @@ class GroupsController < Groups::ApplicationController
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
+ include PreviewMarkdown
respond_to :html
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index a8ebdf5a4a9..f7a9c98629d 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,4 +1,6 @@
class Projects::WikisController < Projects::ApplicationController
+ include PreviewMarkdown
+
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
@@ -92,17 +94,6 @@ class Projects::WikisController < Projects::ApplicationController
def git_access
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
- references: {
- users: result[:users]
- }
- }
- end
-
private
def load_project_wiki
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a738ca9f361..e90b75672ae 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,6 +1,7 @@
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
+ include PreviewMarkdown
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create]
@@ -258,18 +259,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text]),
- references: {
- users: result[:users],
- commands: view_context.markdown(result[:commands])
- }
- }
- end
-
private
# Render project landing depending of which features are available
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index c1cdc7c9831..be2d3f638ff 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -4,6 +4,7 @@ class SnippetsController < ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
+ include PreviewMarkdown
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
@@ -87,17 +88,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path, status: 302
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text], skip_project_check: true),
- references: {
- users: result[:users]
- }
- }
- end
-
protected
def snippet
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7713fb0b9f8..baa2d6e375e 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -314,20 +314,12 @@ module IssuablesHelper
@issuable_templates ||=
case issuable
when Issue
- issue_template_names
+ ref_project.repository.issue_template_names
when MergeRequest
- merge_request_template_names
+ ref_project.repository.merge_request_template_names
end
end
- def merge_request_template_names
- @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
- end
-
- def issue_template_names
- @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
- end
-
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 954d4e4d779..ad0bc2e2ead 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -156,7 +156,9 @@ class Blob < SimpleDelegator
end
def file_type
- Gitlab::FileDetector.type_of(path)
+ name = File.basename(path)
+
+ Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)
end
def video?
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index b85f5dbaf2e..f89e60ad9f4 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -1,4 +1,6 @@
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
+
+ alias_method :user, :resource_owner
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d725c65081d..bf526ca1762 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -34,7 +34,8 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? empty? root_ref has_visible_content?).freeze
+ tag_count avatar exists? empty? root_ref has_visible_content?
+ issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
@@ -50,7 +51,9 @@ class Repository
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
- avatar: :avatar
+ avatar: :avatar,
+ issue_template: :issue_template_names,
+ merge_request_template: :merge_request_template_names
}.freeze
# Wraps around the given method and caches its output in Redis and an instance
@@ -535,6 +538,16 @@ class Repository
end
cache_method :avatar
+ def issue_template_names
+ Gitlab::Template::IssueTemplate.dropdown_names(project)
+ end
+ cache_method :issue_template_names, fallback: []
+
+ def merge_request_template_names
+ Gitlab::Template::MergeRequestTemplate.dropdown_names(project)
+ end
+ cache_method :merge_request_template_names, fallback: []
+
def readme
if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index a110abf8256..8c5821aa870 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -60,13 +60,9 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
- # Verify again that the source branch can be removed, since branch may be protected,
- # or the source branch may have been updated.
- if @merge_request.can_remove_source_branch?(branch_deletion_user)
- DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
- .execute(merge_request.source_branch)
- end
+ if delete_source_branch?
+ DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
+ .execute(merge_request.source_branch)
end
end
@@ -78,6 +74,14 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
+ # Verify again that the source branch can be removed, since branch may be protected,
+ # or the source branch may have been updated, or the user may not have permission
+ #
+ def delete_source_branch?
+ params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) &&
+ @merge_request.can_remove_source_branch?(branch_deletion_user)
+ end
+
# Logs merge error message and cleans `MergeRequest#merge_jid`.
#
def handle_merge_error(log_message:, save_message_on_model: false)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 8d5da459882..be3b4b2ba07 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -390,7 +390,7 @@ class NotificationService
end
def relabeled_resource_email(target, labels, current_user, method)
- recipients = labels.flat_map { |l| l.subscribers(target.project) }
+ recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
recipients, :subscription,
target: target,
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 7f450cd9a93..cc879e5a308 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -10,7 +10,7 @@
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
- = render layout: 'projects/md_preview', locals: { url: '' } do
+ = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index b7a60938132..8eb3f2d5192 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
- %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 3f16885b8e3..574a8f2fa50 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
- %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0934c47a8e2..0a835dcdeb0 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -48,52 +48,54 @@
.tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
- .project-import
- .form-group.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
- Import project from
- .col-sm-12.import-buttons
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn import_github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'Fogbugz')
- %div
- - if gitea_import_enabled?
- = link_to new_import_gitea_url, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
- = icon('git', text: 'Repo by URL')
- - if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
- .col-lg-12
- .js-toggle-content.hide
- %hr
- = render "shared/import_form", f: f
+ .project-import.row
+ .col-sm-12
+ .form-group.import-btn-container.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .import-buttons
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn import_github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = icon('bug', text: 'Fogbugz')
+ %div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.import_git{ type: "button" }
+ = icon('git', text: 'Repo by URL')
+ - if gitlab_project_import_enabled?
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
+ .col-lg-12
+ .js-toggle-content.hide.toggle-import-form
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
.save-project-loader.hide
.center
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 6c3cd6ecefe..cc59f8660fd 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -4,6 +4,9 @@
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_d3'
+ = webpack_bundle_tag 'users'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
diff --git a/changelogs/unreleased/34897-delete-branch-after-merge.yml b/changelogs/unreleased/34897-delete-branch-after-merge.yml
new file mode 100644
index 00000000000..96631aa95c8
--- /dev/null
+++ b/changelogs/unreleased/34897-delete-branch-after-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed 'Removed source branch' checkbox in merge widget being ignored.
+merge_request: 14832
+author:
+type: fixed
diff --git a/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml
new file mode 100644
index 00000000000..b28105556db
--- /dev/null
+++ b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the project import with issues and milestones
+merge_request: 14657
+author:
+type: fixed
diff --git a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml
new file mode 100644
index 00000000000..7e2a7222162
--- /dev/null
+++ b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix flash errors showing up on a non configured prometheus integration
+merge_request: 35652
+author:
+type: fixed
diff --git a/changelogs/unreleased/37660-match-sidebar-colors.yml b/changelogs/unreleased/37660-match-sidebar-colors.yml
new file mode 100644
index 00000000000..d5600f453e7
--- /dev/null
+++ b/changelogs/unreleased/37660-match-sidebar-colors.yml
@@ -0,0 +1,5 @@
+---
+title: Change background color of nav sidebar to match other gl sidebars
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml
new file mode 100644
index 00000000000..c3c38b35fa7
--- /dev/null
+++ b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed duplicate notifications when added multiple labels on an issue
+merge_request: 14798
+author:
+type: fixed
diff --git a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml
new file mode 100644
index 00000000000..5e142a2b4cf
--- /dev/null
+++ b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup data-page attribute after each Karma test
+merge_request: 14742
+author:
+type: fixed
diff --git a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml
new file mode 100644
index 00000000000..d142afa3433
--- /dev/null
+++ b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml
@@ -0,0 +1,6 @@
+---
+title: Removed d3.js from the graph and users bundles and used the common_d3 bundle
+ instead
+merge_request: 14826
+author:
+type: other
diff --git a/changelogs/unreleased/cache-issuable-template-names.yml b/changelogs/unreleased/cache-issuable-template-names.yml
new file mode 100644
index 00000000000..858fdff2db2
--- /dev/null
+++ b/changelogs/unreleased/cache-issuable-template-names.yml
@@ -0,0 +1,5 @@
+---
+title: Cache issue and MR template names in Redis
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/issue-36484.yml b/changelogs/unreleased/issue-36484.yml
new file mode 100644
index 00000000000..a19126e650f
--- /dev/null
+++ b/changelogs/unreleased/issue-36484.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary alt-texts from pipeline emails
+merge_request: 14602
+author: gernberg
+type: fixed
diff --git a/changelogs/unreleased/move_markdown_preview_to_concern.yml b/changelogs/unreleased/move_markdown_preview_to_concern.yml
new file mode 100644
index 00000000000..036e77610b9
--- /dev/null
+++ b/changelogs/unreleased/move_markdown_preview_to_concern.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for markdown preview to group milestones
+merge_request: 14806
+author: Vitaliy @blackst0ne Klachkov
+type: fixed
diff --git a/changelogs/unreleased/replace_explore_projects-feature.yml b/changelogs/unreleased/replace_explore_projects-feature.yml
new file mode 100644
index 00000000000..85ef045fb4b
--- /dev/null
+++ b/changelogs/unreleased/replace_explore_projects-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'features/explore/projects.feature' spinach test with an rspec analog
+merge_request: 14755
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 23052a6c6dc..8cc30bfcc50 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,6 +1,8 @@
require 'constraints/group_url_constrainer'
-resources :groups, only: [:index, :new, :create]
+resources :groups, only: [:index, :new, :create] do
+ post :preview_markdown
+end
scope(path: 'groups/*group_id',
module: :groups,
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 8cded750a66..a71794b379d 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -84,6 +84,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
+ users: './users/index.js',
performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js',
},
@@ -215,7 +216,9 @@ var config = {
name: 'common_d3',
chunks: [
'graphs',
+ 'graphs_show',
'monitoring',
+ 'users',
],
}),
diff --git a/doc/development/README.md b/doc/development/README.md
index e2d0c6c2056..36096842344 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -21,7 +21,6 @@
## Backend guides
-- [Testing standards and style guidelines](testing_guide/index.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
@@ -67,6 +66,11 @@
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
+## Testing guides
+
+- [Testing standards and style guidelines](testing_guide/index.md)
+- [Frontend testing standards and style guidelines](testing_guide/frontend_testing.md)
+
## Documentation guides
- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index 79707aaf671..b34ec754742 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -58,6 +58,18 @@ For example, in French we translate `you` as the informal `tu`.
You can refer to other translated strings and notes in the glossary to assist determining a
suitable level of formality.
+### Inclusive language
+
+[Diversity] is one of GitLab's values.
+We ask you to avoid translations which exclude people based on their gender or ethnicity.
+In languages which distinguish between a male and female form,
+use both or choose a neutral formulation.
+
+For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female).
+Therefore "create a new user" would translate into "Benutzer(in) anlegen".
+
+[Diversity]: https://about.gitlab.com/handbook/values/#diversity
+
### Updating the glossary
To propose additions to the glossary please
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 38b1fe1a193..8045bbad7ba 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -84,6 +84,7 @@ test should be re-implemented using RSpec instead.
[^1]: /ci/yaml/README.html#dependencies
+[rails]: http://rubyonrails.org/
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/
diff --git a/doc/install/installation.md b/doc/install/installation.md
index af6c797dc00..2c93297ca2f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -121,7 +121,7 @@ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
GitLab Shell is called from OpenSSH, and having a version manager can prevent
pushing and pulling over SSH. Version managers are not supported and we strongly
-advise everyone to follow the instructions below to use a system Ruby.
+advise everyone to follow the instructions below to use a system Ruby.
Linux distributions generally have older versions of Ruby available, so these
instructions are designed to install Ruby from the official source code.
@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-1-stable gitlab
-**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/integration/google.md b/doc/integration/google.md
index d5b523e6dc0..0611cbb59dc 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -1,83 +1,92 @@
# Google OAuth2 OmniAuth Provider
-To enable the Google OAuth2 OmniAuth provider you must register your application with Google. Google will generate a client ID and secret key for you to use.
-
-1. Sign in to the [Google Developers Console](https://console.developers.google.com/) with the Google account you want to use to register GitLab.
-
-1. Select "Create Project".
-
-1. Provide the project information
- - Project name: 'GitLab' works just fine here.
- - Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one.
-1. Refresh the page. You should now see your new project in the list. Click on the project.
-
-1. Select the "Google APIs" tab in the Overview.
-
-1. Select and enable the following Google APIs - listed under "Popular APIs"
- - Enable `Contacts API`
- - Enable `Google+ API`
+To enable the Google OAuth2 OmniAuth provider you must register your application
+with Google. Google will generate a client ID and secret key for you to use.
+
+## Enabling Google OAuth
+
+In Google's side:
+
+1. Navigate to the [cloud resource manager](https://console.cloud.google.com/cloud-resource-manager) page
+1. Select **Create Project**
+1. Provide the project information:
+ - **Project name** - "GitLab" works just fine here.
+ - **Project ID** - Must be unique to all Google Developer registered applications.
+ Google provides a randomly generated Project ID by default. You can use
+ the randomly generated ID or choose a new one.
+1. Refresh the page and you should see your new project in the list
+1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard)
+1. Select the previously created project form the upper left corner
+1. Select **Credentials** from the sidebar
+1. Select **OAuth consent screen** and fill the form with the required information
+1. In the **Credentials** tab, select **Create credentials > OAuth client ID**
+1. Fill in the required information
+ - **Application type** - Choose "Web Application"
+ - **Name** - Use the default one or provide your own
+ - **Authorized JavaScript origins** -This isn't really used by GitLab but go
+ ahead and put `https://gitlab.example.com`
+ - **Authorized redirect URIs** - Enter your domain name followed by the
+ callback URIs one at a time:
-1. Select "Credentials" in the submenu.
+ ```
+ https://gitlab.example.com/users/auth/google_oauth2/callback
+ https://gitlab.exampl.com/-/google_api/auth/callback
+ ```
-1. Select "Create New Client ID".
+1. You should now be able to see a Client ID and Client secret. Note them down
+ or keep this page open as you will need them later.
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Google Cloud APIs > Container Engine API > Enable**
-1. Fill in the required information
- - Application type: "Web Application"
- - Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here.
- - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback'
-1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png)
+On your GitLab server:
-1. On your GitLab server, open the configuration file.
+1. Open the configuration file.
- For omnibus package:
+ For Omnibus GitLab:
```sh
- sudo editor /etc/gitlab/gitlab.rb
+ sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
- cd /home/git/gitlab
-
- sudo -u git -H editor config/gitlab.yml
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
-
-1. Add the provider configuration:
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+1. Add the provider configuration:
- For omnibus package:
+ For Omnibus GitLab:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "google_oauth2",
- "app_id" => "YOUR_APP_ID",
- "app_secret" => "YOUR_APP_SECRET",
- "args" => { "access_type" => "offline", "approval_prompt" => '' }
- }
- ]
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "google_oauth2",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
+ "args" => { "access_type" => "offline", "approval_prompt" => '' }
+ }
+ ]
```
For installations from source:
```
- - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
- app_secret: 'YOUR_APP_SECRET',
- args: { access_type: 'offline', approval_prompt: '' } }
+ - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
+ args: { access_type: 'offline', approval_prompt: '' } }
```
-1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10.
-
-1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10.
-
-1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses.
+1. Change `YOUR_APP_ID` to the client ID from the Google Developer page
+1. Similarly, change `YOUR_APP_SECRET` to the client secret
+1. Make sure that you configure GitLab to use an FQDN as Google will not accept
+ raw IP addresses.
For Omnibus packages:
```ruby
- external_url 'https://gitlab.example.com'
+ external_url 'https://gitlab.example.com'
```
For installations from source:
@@ -88,21 +97,32 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
```
1. Save the configuration file.
-
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
-On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+On the sign in page there should now be a Google icon below the regular sign in
+form. Click the icon to begin the authentication process. Google will ask the
+user to sign in and authorize the GitLab application. If everything goes well
+the user will be returned to GitLab and will be signed in.
## Further Configuration
-This further configuration is not required for Google authentication to function but it is strongly recommended. Taking these steps will increase usability for users by providing a little more recognition and branding.
-
-At this point, when users first try to authenticate to your GitLab installation with Google they will see a generic application name on the prompt screen. The prompt informs the user that "Project Default Service Account" would like to access their account. "Project Default Service Account" isn't very recognizable and may confuse or cause users to be concerned. This is easily changeable.
-
-1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window).
-1. Scroll down until you find "Product Name". Change the product name to something more descriptive.
-1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users.
+This further configuration is not required for Google authentication to function
+but it is strongly recommended. Taking these steps will increase usability for
+users by providing a little more recognition and branding.
+
+At this point, when users first try to authenticate to your GitLab installation
+with Google they will see a generic application name on the prompt screen. The
+prompt informs the user that "Project Default Service Account" would like to
+access their account. "Project Default Service Account" isn't very recognizable
+and may confuse or cause users to be concerned. This is easily changeable:
+
+1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for
+ instructions on how to get here if you closed your window).
+1. Scroll down until you find "Product Name". Change the product name to
+ something more descriptive.
+1. Add any additional information as you wish - homepage, logo, privacy policy,
+ etc. None of this is required, but it may help your users.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
index 4a9384f3ad6..dc14c779026 100644
--- a/doc/update/10.0-to-10.1.md
+++ b/doc/update/10.0-to-10.1.md
@@ -150,7 +150,7 @@ sudo -u git -H make
#### New Gitaly configuration options required
-In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
```shell
echo '
@@ -335,11 +335,11 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
-## Things went south? Revert to previous version (9.5)
+## Things went south? Revert to previous version (10.0)
### 1. Revert the code to the previous version
-Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
+Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 5bd326a426f..2206b2860f4 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -155,7 +155,7 @@ comments in greater detail.
## Image discussions
-> [Introduced][ce-14531] in GitLab 10.1.
+> [Introduced][ce-14061] in GitLab 10.1.
Sometimes a discussion is revolved around an image. With image discussions,
you can easily target a specific coordinate of an image and start a discussion
@@ -227,6 +227,7 @@ edit existing comments. Non-team members are restricted from adding or editing c
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
+[ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061
[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index c4e41c8e9bf..c03700a3501 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -76,6 +76,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ |
+| Manage clusters | | | | ✓ | ✓ |
## Project features permissions
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
new file mode 100644
index 00000000000..7d9e771f570
--- /dev/null
+++ b/doc/user/project/clusters/index.md
@@ -0,0 +1,90 @@
+# Connecting GitLab with GKE
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1.
+
+CAUTION: **Warning:**
+The Cluster integration is currently in **Beta**.
+
+Connect your project to Google Container Engine (GKE) in a few steps.
+
+With a cluster associated to your project, you can use Review Apps, deploy your
+applications, run your pipelines, and much more in an easy way.
+
+NOTE: **Note:**
+The Cluster integration will eventually supersede the
+[Kubernetes integration](../integrations/kubernetes.md). For the moment,
+you can create only one cluster.
+
+## Prerequisites
+
+In order to be able to manage your GKE cluster through GitLab, the following
+prerequisites must be met:
+
+- The [Google authentication integration](../../../integration/google.md) must
+ be enabled in GitLab at the instance level. If that's not the case, ask your
+ administrator to enable it.
+- Your associated Google account must have the right privileges to manage
+ clusters on GKE. That would mean that a
+ [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
+ must be set up.
+- You must have Master [permissions] in order to be able to access the **Cluster**
+ page.
+
+If all of the above requirements are met, you can proceed to add a new cluster.
+
+## Adding a cluster
+
+NOTE: **Note:**
+You need Master [permissions] and above to add a cluster.
+
+To add a new cluster:
+
+1. Navigate to your project's **CI/CD > Cluster** page.
+1. Connect your Google account if you haven't done already by clicking the
+ "Sign-in with Google" button.
+1. Fill in the requested values:
+ - **Cluster name** (required) - The name you wish to give the cluster.
+ - **GCP project ID** (required) - The ID of the project you created in your GCP
+ console that will host the Kubernetes cluster. This must **not** be confused
+ with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - The zone under which the cluster will be created. Read more about
+ [the available zones](https://cloud.google.com/compute/docs/regions-zones/).
+ - **Number of nodes** - The number of nodes you wish the cluster to have.
+ - **Machine type** - The machine type of the Virtual Machine instance that
+ the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types).
+ - **Project namespace** - The unique namespace for this project. By default you
+ don't have to fill it in; by leaving it blank, GitLab will create one for you.
+1. Click the **Create cluster** button.
+
+After a few moments your cluster should be created. If something goes wrong,
+you will be notified.
+
+Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration).
+
+## Enabling or disabling the Cluster integration
+
+After you have successfully added your cluster information, you can enable the
+Cluster integration:
+
+1. Click the "Enabled/Disabled" switch
+1. Hit **Save** for the changes to take effect
+
+You can now start using your Kubernetes cluster for your deployments.
+
+To disable the Cluster integration, follow the same procedure.
+
+## Removing the Cluster integration
+
+NOTE: **Note:**
+You need Master [permissions] and above to remove a cluster integration.
+
+NOTE: **Note:**
+When you remove a cluster, you only remove its relation to GitLab, not the
+cluster itself. To remove the cluster, you can do so by visiting the GKE
+dashboard or using `kubectl`.
+
+To remove the Cluster integration from your project, simply click on the
+**Remove integration** button. You will then be able to follow the procedure
+and [add a cluster](#adding-a-cluster) again.
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 03bbc46bd8c..97d0d529886 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -63,6 +63,8 @@ common actions on issues or merge requests
browse, and download job artifacts
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
+ - [GKE cluster integration](clusters/index.md): Connecting your GitLab project
+ with Google Container Engine
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
deleted file mode 100644
index 4e0f4486ab7..00000000000
--- a/features/explore/projects.feature
+++ /dev/null
@@ -1,144 +0,0 @@
-@public
-Feature: Explore Projects
- Background:
- Given public project "Community"
- And internal project "Internal"
- And private project "Enterprise"
-
- Scenario: I visit public area
- Given archived project "Archive"
- When I visit the public projects area
- Then I should see project "Community"
- And I should not see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: I visit public project page
- When I visit project "Community" page
- Then I should see project "Community" home page
-
- Scenario: I visit internal project page
- When I visit project "Internal" page
- Then I should be redirected to sign in page
-
- Scenario: I visit private project page
- When I visit project "Enterprise" page
- Then I should be redirected to sign in page
-
- Scenario: I visit an empty public project page
- Given public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with http clone info
-
- Scenario: I visit an empty public project page as user with no ssh-keys
- Given I sign in as a user
- And I have no ssh keys
- And public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with http clone info
-
- Scenario: I visit an empty public project page as user with an ssh-key
- Given I sign in as a user
- And I have an ssh key
- And public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with ssh clone info
-
- Scenario: I visit public area as user
- Given archived project "Archive"
- And I sign in as a user
- When I visit the public projects area
- Then I should see project "Community"
- And I should see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: I visit internal project page as user
- Given I sign in as a user
- When I visit project "Internal" page
- Then I should see project "Internal" home page
-
- Scenario: I visit public project page
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an http link to the repository
-
- Scenario: I visit public project page as user with no ssh-keys
- Given I sign in as a user
- And I have no ssh keys
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an http link to the repository
-
- Scenario: I visit public project page as user with an ssh-key
- Given I sign in as a user
- And I have an ssh key
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an ssh link to the repository
-
- Scenario: I visit an empty public project page
- Given public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
-
- Scenario: I visit public project issues page as a non authorized user
- Given I visit project "Community" page
- Then I should not see command line instructions
- And I visit "Community" issues page
- Then I should see list of issues for "Community" project
-
- Scenario: I visit public project issues page as authorized user
- Given I sign in as a user
- Given I visit project "Community" page
- And I visit "Community" issues page
- Then I should see list of issues for "Community" project
-
- Scenario: I visit internal project issues page as authorized user
- Given I sign in as a user
- Given I visit project "Internal" page
- And I visit "Internal" issues page
- Then I should see list of issues for "Internal" project
-
- Scenario: I visit public project merge requests page as an authorized user
- Given I sign in as a user
- Given I visit project "Community" page
- And I visit "Community" merge requests page
- And project "Community" has "Bug fix" open merge request
- Then I should see list of merge requests for "Community" project
-
- Scenario: I visit public project merge requests page as a non authorized user
- Given I visit project "Community" page
- And I visit "Community" merge requests page
- And project "Community" has "Bug fix" open merge request
- Then I should see list of merge requests for "Community" project
-
- Scenario: I visit internal project merge requests page as an authorized user
- Given I sign in as a user
- Given I visit project "Internal" page
- And I visit "Internal" merge requests page
- And project "Internal" has "Feature implemented" open merge request
- Then I should see list of merge requests for "Internal" project
-
- Scenario: Trending page
- Given archived project "Archive"
- And project "Archive" has comments
- And I sign in as a user
- And project "Community" has comments
- And trending projects are refreshed
- When I visit the explore trending projects
- Then I should see project "Community"
- And I should not see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: Most starred page
- Given archived project "Archive"
- And I sign in as a user
- When I visit the explore starred projects
- Then I should see project "Community"
- And I should see project "Internal"
- And I should not see project "Archive"
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
deleted file mode 100644
index 962e39dde9a..00000000000
--- a/features/steps/explore/projects.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedUser
-
- step 'I should see project "Empty Public Project"' do
- expect(page).to have_content "Empty Public Project"
- end
-
- step 'I should see public project details' do
- expect(page).to have_content '32 branches'
- expect(page).to have_content '16 tags'
- end
-
- step 'I should see project readme' do
- expect(page).to have_content 'README.md'
- end
-
- step 'I should see empty public project details' do
- expect(page).not_to have_content 'Git global setup'
- end
-
- step 'I should see empty public project details with http clone info' do
- project = Project.find_by(name: 'Empty Public Project')
- page.all(:css, '.git-empty .clone').each do |element|
- expect(element.text).to include(project.http_url_to_repo)
- end
- end
-
- step 'I should see empty public project details with ssh clone info' do
- project = Project.find_by(name: 'Empty Public Project')
- page.all(:css, '.git-empty .clone').each do |element|
- expect(element.text).to include(project.url_to_repo)
- end
- end
-
- step 'I should see project "Community" home page' do
- page.within '.breadcrumbs .breadcrumb-item-text' do
- expect(page).to have_content 'Community'
- end
- end
-
- step 'I should see project "Internal" home page' do
- page.within '.breadcrumbs .breadcrumb-item-text' do
- expect(page).to have_content 'Internal'
- end
- end
-
- step 'I should see an http link to the repository' do
- project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.http_url_to_repo)
- end
-
- step 'I should see an ssh link to the repository' do
- project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.url_to_repo)
- end
-
- step 'I visit "Community" issues page' do
- create(:issue,
- title: "Bug",
- project: public_project
- )
- create(:issue,
- title: "New feature",
- project: public_project
- )
- visit project_issues_path(public_project)
- end
-
- step 'I should see list of issues for "Community" project' do
- expect(page).to have_content "Bug"
- expect(page).to have_content public_project.name
- expect(page).to have_content "New feature"
- end
-
- step 'I visit "Internal" issues page' do
- create(:issue,
- title: "Internal Bug",
- project: internal_project
- )
- create(:issue,
- title: "New internal feature",
- project: internal_project
- )
- visit project_issues_path(internal_project)
- end
-
- step 'I should see list of issues for "Internal" project' do
- expect(page).to have_content "Internal Bug"
- expect(page).to have_content internal_project.name
- expect(page).to have_content "New internal feature"
- end
-
- step 'I visit "Community" merge requests page' do
- visit project_merge_requests_path(public_project)
- end
-
- step 'project "Community" has "Bug fix" open merge request' do
- create(:merge_request,
- title: "Bug fix for public project",
- source_project: public_project,
- target_project: public_project
- )
- end
-
- step 'I should see list of merge requests for "Community" project' do
- expect(page).to have_content public_project.name
- expect(page).to have_content public_merge_request.source_project.name
- end
-
- step 'I visit "Internal" merge requests page' do
- visit project_merge_requests_path(internal_project)
- end
-
- step 'project "Internal" has "Feature implemented" open merge request' do
- create(:merge_request,
- title: "Feature implemented",
- source_project: internal_project,
- target_project: internal_project
- )
- end
-
- step 'I should see list of merge requests for "Internal" project' do
- expect(page).to have_content internal_project.name
- expect(page).to have_content internal_merge_request.source_project.name
- end
-
- def internal_project
- @internal_project ||= Project.find_by!(name: 'Internal')
- end
-
- def public_project
- @public_project ||= Project.find_by!(name: 'Community')
- end
-
- def internal_merge_request
- @internal_merge_request ||= MergeRequest.find_by!(title: 'Feature implemented')
- end
-
- def public_merge_request
- @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project')
- end
-end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index be69a96c3ee..dc0e3ac59a5 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -454,19 +454,6 @@ module SharedPaths
# ----------------------------------------
# Public Projects
# ----------------------------------------
-
- step 'I visit the public projects area' do
- visit explore_projects_path
- end
-
- step 'I visit the explore trending projects' do
- visit trending_explore_projects_path
- end
-
- step 'I visit the explore starred projects' do
- visit starred_explore_projects_path
- end
-
step 'I visit the public groups area' do
visit explore_groups_path
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 96cc0745e97..5e4edaf99a6 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -112,10 +112,6 @@ module SharedProject
# Visibility of archived project
# ----------------------------------------
- step 'archived project "Archive"' do
- create(:project, :archived, :public, :repository, name: 'Archive')
- end
-
step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace
@@ -126,11 +122,6 @@ module SharedProject
expect(page).to have_content project.name_with_namespace
end
- step 'project "Archive" has comments' do
- project = Project.find_by(name: "Archive")
- 2.times { create(:note_on_issue, project: project) }
- end
-
# ----------------------------------------
# Visibility level
# ----------------------------------------
@@ -209,15 +200,6 @@ module SharedProject
create :project_empty_repo, :public, name: "Empty Public Project"
end
- step 'project "Community" has comments' do
- project = Project.find_by(name: "Community")
- 2.times { create(:note_on_issue, project: project) }
- end
-
- step 'trending projects are refreshed' do
- TrendingProject.refresh!
- end
-
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug')
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index e79f988f549..87b9db66efd 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -42,6 +42,38 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
+ def find_current_user
+ user =
+ find_user_from_private_token ||
+ find_user_from_oauth_token ||
+ find_user_from_warden
+
+ return nil unless user
+
+ raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+
+ user
+ end
+
+ def private_token
+ params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+ end
+
+ private
+
+ def find_user_from_private_token
+ token_string = private_token.to_s
+ return nil unless token_string.present?
+
+ user =
+ find_user_by_authentication_token(token_string) ||
+ find_user_by_personal_access_token(token_string)
+
+ raise UnauthorizedError unless user
+
+ user
+ end
+
# Invokes the doorkeeper guard.
#
# If token is presented and valid, then it sets @current_user.
@@ -60,70 +92,89 @@ module API
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
- def doorkeeper_guard(scopes: [])
- access_token = find_access_token
- return nil unless access_token
-
- case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
- when AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
-
- when AccessTokenValidationService::EXPIRED
- raise ExpiredError
+ def find_user_from_oauth_token
+ access_token = find_oauth_access_token
+ return unless access_token
- when AccessTokenValidationService::REVOKED
- raise RevokedError
+ find_user_by_access_token(access_token)
+ end
- when AccessTokenValidationService::VALID
- User.find(access_token.resource_owner_id)
- end
+ def find_user_by_authentication_token(token_string)
+ User.find_by_authentication_token(token_string)
end
- def find_user_by_private_token(scopes: [])
- token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+ def find_user_by_personal_access_token(token_string)
+ access_token = PersonalAccessToken.find_by_token(token_string)
+ return unless access_token
- return nil unless token_string.present?
+ find_user_by_access_token(access_token)
+ end
- user =
- find_user_by_authentication_token(token_string) ||
- find_user_by_personal_access_token(token_string, scopes)
+ # Check the Rails session for valid authentication details
+ def find_user_from_warden
+ warden.try(:authenticate) if verified_request?
+ end
- raise UnauthorizedError unless user
+ def warden
+ env['warden']
+ end
- user
+ # Check if the request is GET/HEAD, or if CSRF token is valid.
+ def verified_request?
+ Gitlab::RequestForgeryProtection.verified?(env)
end
- private
+ def find_oauth_access_token
+ return @oauth_access_token if defined?(@oauth_access_token)
- def find_user_by_authentication_token(token_string)
- User.find_by_authentication_token(token_string)
- end
+ token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
+ return @oauth_access_token = nil unless token
- def find_user_by_personal_access_token(token_string, scopes)
- access_token = PersonalAccessToken.active.find_by_token(token_string)
- return unless access_token
+ @oauth_access_token = OauthAccessToken.by_token(token)
+ raise UnauthorizedError unless @oauth_access_token
- if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes)
- User.find(access_token.user_id)
- end
+ @oauth_access_token.revoke_previous_refresh_token!
+ @oauth_access_token
end
- def find_access_token
- return @access_token if defined?(@access_token)
+ def find_user_by_access_token(access_token)
+ scopes = scopes_registered_for_endpoint
- token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
- return @access_token = nil unless token
+ case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
+
+ when AccessTokenValidationService::EXPIRED
+ raise ExpiredError
- @access_token = Doorkeeper::AccessToken.by_token(token)
- raise UnauthorizedError unless @access_token
+ when AccessTokenValidationService::REVOKED
+ raise RevokedError
- @access_token.revoke_previous_refresh_token!
- @access_token
+ when AccessTokenValidationService::VALID
+ access_token.user
+ end
end
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
+
+ # An array of scopes that were registered (using `allow_access_with_scope`)
+ # for the current endpoint class. It also returns scopes registered on
+ # `API::API`, since these are meant to apply to all API routes.
+ def scopes_registered_for_endpoint
+ @scopes_registered_for_endpoint ||=
+ begin
+ endpoint_classes = [options[:for].presence, ::API::API].compact
+ endpoint_classes.reduce([]) do |memo, endpoint|
+ if endpoint.respond_to?(:allowed_scopes)
+ memo.concat(endpoint.allowed_scopes)
+ else
+ memo
+ end
+ end
+ end
+ end
end
module ClassMethods
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a87297a604c..2b316b58ed9 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,8 +3,6 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- UnauthorizedError = Class.new(StandardError)
-
SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
@@ -379,47 +377,16 @@ module API
private
- def private_token
- params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
- end
-
- def warden
- env['warden']
- end
-
- # Check if the request is GET/HEAD, or if CSRF token is valid.
- def verified_request?
- Gitlab::RequestForgeryProtection.verified?(env)
- end
-
- # Check the Rails session for valid authentication details
- def find_user_from_warden
- warden.try(:authenticate) if verified_request?
- end
-
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
- rescue APIGuard::UnauthorizedError, UnauthorizedError
+ rescue APIGuard::UnauthorizedError
unauthorized!
end
end
- def find_current_user
- user =
- find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
- doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
- find_user_from_warden
-
- return nil unless user
-
- raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
-
- user
- end
-
def sudo!
return unless sudo_identifier
return unless initial_current_user
@@ -479,22 +446,5 @@ module API
exception.status == 500
end
-
- # An array of scopes that were registered (using `allow_access_with_scope`)
- # for the current endpoint class. It also returns scopes registered on
- # `API::API`, since these are meant to apply to all API routes.
- def scopes_registered_for_endpoint
- @scopes_registered_for_endpoint ||=
- begin
- endpoint_classes = [options[:for].presence, ::API::API].compact
- endpoint_classes.reduce([]) do |memo, endpoint|
- if endpoint.respond_to?(:allowed_scopes)
- memo.concat(endpoint.allowed_scopes)
- else
- memo
- end
- end
- end
- end
end
end
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index a8cb7fc3fe7..0e9ef4f897c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -6,31 +6,33 @@ module Gitlab
module FileDetector
PATTERNS = {
# Project files
- readme: /\Areadme/i,
- changelog: /\A(changelog|history|changes|news)/i,
- license: /\A(licen[sc]e|copying)(\..+|\z)/i,
- contributing: /\Acontributing/i,
+ readme: /\Areadme[^\/]*\z/i,
+ changelog: /\A(changelog|history|changes|news)[^\/]*\z/i,
+ license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i,
+ contributing: /\Acontributing[^\/]*\z/i,
version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/,
+ issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/,
+ merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/,
# Configuration files
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
- route_map: 'route-map.yml',
+ route_map: '.gitlab/route-map.yml',
# Dependency files
- cartfile: /\ACartfile/,
+ cartfile: /\ACartfile[^\/]*\z/,
composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock',
- gemspec: /\.gemspec\z/,
+ gemspec: /\A[^\/]*\.gemspec\z/,
godeps_json: 'Godeps.json',
package_json: 'package.json',
podfile: 'Podfile',
- podspec_json: /\.podspec\.json\z/,
- podspec: /\.podspec\z/,
- requirements_txt: /requirements\.txt\z/,
+ podspec_json: /\A[^\/]*\.podspec\.json\z/,
+ podspec: /\A[^\/]*\.podspec\z/,
+ requirements_txt: /\A[^\/]*requirements\.txt\z/,
yarn_lock: 'yarn.lock'
}.freeze
@@ -63,13 +65,11 @@ module Gitlab
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
- name = File.basename(path)
-
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
- name =~ search
+ path =~ search
else
- name.casecmp(search) == 0
+ path.casecmp(search) == 0
end
return type if did_match
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 86c29a15a3d..a6b2d189f18 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1086,6 +1086,12 @@ module Gitlab
@has_visible_content = has_local_branches?
end
+ def fetch(remote = 'origin')
+ args = %W(#{Gitlab.config.git.bin_path} fetch #{remote})
+
+ popen(args, @path).last.zero?
+ end
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3bc095a99a9..639f4f0c3f0 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -2,7 +2,7 @@ module Gitlab
module ImportExport
class ProjectTreeRestorer
# Relations which cannot have both group_id and project_id at the same time
- RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze
+ RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index a76cf1addc0..469b230377d 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -37,7 +37,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id)
+ @relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@user = user
@project = project
@@ -58,22 +58,21 @@ module Gitlab
private
def setup_models
- if @relation_name == :notes
- set_note_author
-
- # attachment is deprecated and note uploads are handled by Markdown uploader
- @relation_hash['attachment'] = nil
+ case @relation_name
+ when :merge_request_diff then setup_st_diff_commits
+ when :merge_request_diff_files then setup_diff
+ when :notes then setup_note
+ when :project_label, :project_labels then setup_label
+ when :milestone, :milestones then setup_milestone
+ else
+ @relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
- handle_group_label if group_label?
reset_tokens!
remove_encrypted_attributes!
-
- set_st_diff_commits if @relation_name == :merge_request_diff
- set_diff if @relation_name == :merge_request_diff_files
end
def update_user_references
@@ -84,6 +83,12 @@ module Gitlab
end
end
+ def setup_note
+ set_note_author
+ # attachment is deprecated and note uploads are handled by Markdown uploader
+ @relation_hash['attachment'] = nil
+ end
+
# Sets the author for a note. If the user importing the project
# has admin access, an actual mapping with new project members
# will be used. Otherwise, a note stating the original author name
@@ -136,11 +141,9 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
- def group_label?
- @relation_hash['type'] == 'GroupLabel'
- end
+ def setup_label
+ return unless @relation_hash['type'] == 'GroupLabel'
- def handle_group_label
# If there's no group, move the label to a project label
if @relation_hash['group_id']
@relation_hash['project_id'] = nil
@@ -150,6 +153,14 @@ module Gitlab
end
end
+ def setup_milestone
+ if @relation_hash['group_id']
+ @relation_hash['group_id'] = @project.group.id
+ else
+ @relation_hash['project_id'] = @project.id
+ end
+ end
+
def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
@@ -198,14 +209,14 @@ module Gitlab
relation_class: relation_class)
end
- def set_st_diff_commits
+ def setup_st_diff_commits
@relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
end
- def set_diff
+ def setup_diff
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
@@ -250,7 +261,13 @@ module Gitlab
end
def find_or_create_object!
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_attributes = if @relation_name == :group_label
+ %w[title group_id]
+ elsif parsed_relation_hash['project_id']
+ %w[title project_id]
+ else
+ %w[title group_id]
+ end
finder_hash = parsed_relation_hash.slice(*finder_attributes)
if label?
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index 0b43377a579..ae136202f0c 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -25,9 +25,9 @@ module Gitlab
end
TEMPLATES_TABLE = [
- ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes a MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
- ProjectTemplate.new('spring', 'Spring', 'Includes a MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
- ProjectTemplate.new('express', 'NodeJS Express', 'Includes a MVC structure, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
+ ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
+ ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
+ ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
].freeze
class << self
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 2c732aaf4ed..7c4a22c94c2 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -73,6 +73,12 @@ FactoryGirl.define do
merge_user author
end
+ trait :remove_source_branch do
+ merge_params do
+ { 'force_remove_source_branch' => '1' }
+ end
+ end
+
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index 533df7a325c..a6329b5c78d 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -1,14 +1,15 @@
require 'spec_helper'
feature 'Dashboard Groups page', :js do
- let!(:user) { create :user }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, :nested) }
- let!(:another_group) { create(:group) }
+ let(:user) { create :user }
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, :nested) }
+ let(:another_group) { create(:group) }
it 'shows groups user is member of' do
group.add_owner(user)
nested_group.add_owner(user)
+ expect(another_group).to be_persisted
sign_in(user)
visit dashboard_groups_path
@@ -22,6 +23,7 @@ feature 'Dashboard Groups page', :js do
before do
group.add_owner(user)
nested_group.add_owner(user)
+ expect(another_group).to be_persisted
sign_in(user)
@@ -51,7 +53,7 @@ feature 'Dashboard Groups page', :js do
end
end
- describe 'group with subgroups' do
+ describe 'group with subgroups', :nested_groups do
let!(:subgroup) { create(:group, :public, parent: group) }
before do
@@ -90,7 +92,8 @@ feature 'Dashboard Groups page', :js do
end
describe 'when using pagination' do
- let(:group2) { create(:group) }
+ let(:group) { create(:group, created_at: 5.days.ago) }
+ let(:group2) { create(:group, created_at: 2.days.ago) }
before do
group.add_owner(user)
@@ -102,12 +105,9 @@ feature 'Dashboard Groups page', :js do
visit dashboard_groups_path
end
- it 'shows pagination' do
- expect(page).to have_selector('.gl-pagination')
+ it 'loads results for next page' do
expect(page).to have_selector('.gl-pagination .page', count: 2)
- end
- it 'loads results for next page' do
# Check first page
expect(page).to have_content(group2.full_name)
expect(page).to have_selector("#group-#{group2.id}")
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
new file mode 100644
index 00000000000..6ac9497b024
--- /dev/null
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'User explores projects' do
+ set(:archived_project) { create(:project, :archived) }
+ set(:internal_project) { create(:project, :internal) }
+ set(:private_project) { create(:project, :private) }
+ set(:public_project) { create(:project, :public) }
+
+ shared_examples_for 'shows public projects' do
+ it 'shows projects' do
+ expect(page).to have_content(public_project.title)
+ expect(page).not_to have_content(internal_project.title)
+ expect(page).not_to have_content(private_project.title)
+ expect(page).not_to have_content(archived_project.title)
+ end
+ end
+
+ shared_examples_for 'shows public and internal projects' do
+ it 'shows projects' do
+ expect(page).to have_content(public_project.title)
+ expect(page).to have_content(internal_project.title)
+ expect(page).not_to have_content(private_project.title)
+ expect(page).not_to have_content(archived_project.title)
+ end
+ end
+
+ context 'when not signed in' do
+ context 'when viewing public projects' do
+ before do
+ visit(explore_projects_path)
+ end
+
+ include_examples 'shows public projects'
+ end
+ end
+
+ context 'when signed in' do
+ set(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when viewing public projects' do
+ before do
+ visit(explore_projects_path)
+ end
+
+ include_examples 'shows public and internal projects'
+ end
+
+ context 'when viewing most starred projects' do
+ before do
+ visit(starred_explore_projects_path)
+ end
+
+ include_examples 'shows public and internal projects'
+ end
+
+ context 'when viewing trending projects' do
+ before do
+ [archived_project, public_project].each { |project| create(:note_on_issue, project: project) }
+
+ TrendingProject.refresh!
+
+ visit(trending_explore_projects_path)
+ end
+
+ include_examples 'shows public projects'
+ end
+ end
+end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 56144d17d4f..12aa54a3da1 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -18,6 +18,27 @@ feature 'Group milestones', :js do
visit new_group_milestone_path(group)
end
+ it 'renders description preview' do
+ form = find('.gfm-form')
+
+ form.fill_in(:milestone_description, with: '')
+
+ click_link('Preview')
+
+ preview = find('.js-md-preview')
+
+ expect(preview).to have_content('Nothing to preview.')
+
+ click_link('Write')
+
+ form.fill_in(:milestone_description, with: ':+1: Nice')
+
+ click_link('Preview')
+
+ expect(preview).to have_css('gl-emoji')
+ expect(find('#milestone_description', visible: false)).not_to be_visible
+ end
+
it 'creates milestone with start date' do
fill_in 'Title', with: 'testing'
find('#milestone_start_date').click
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
deleted file mode 100644
index 9fc03f49f5b..00000000000
--- a/spec/features/projects/issues/list_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-feature 'Issues List' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- background do
- project.team << [user, :developer]
-
- sign_in(user)
- end
-
- scenario 'user does not see create new list button' do
- create(:issue, project: project)
-
- visit project_issues_path(project)
-
- expect(page).not_to have_selector('.js-new-board-list')
- end
-end
diff --git a/spec/features/projects/issues/user_views_issues_spec.rb b/spec/features/projects/issues/user_views_issues_spec.rb
new file mode 100644
index 00000000000..d35009b8974
--- /dev/null
+++ b/spec/features/projects/issues/user_views_issues_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'User views issues' do
+ set(:user) { create(:user) }
+
+ shared_examples_for 'shows issues' do
+ it 'shows issues' do
+ expect(page).to have_content(project.name)
+ .and have_content(issue1.title)
+ .and have_content(issue2.title)
+ .and have_no_selector('.js-new-board-list')
+ end
+ end
+
+ context 'when project is public' do
+ set(:project) { create(:project_empty_repo, :public) }
+ set(:issue1) { create(:issue, project: project) }
+ set(:issue2) { create(:issue, project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+
+ context 'when not signed in' do
+ before do
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+ end
+
+ context 'when project is internal' do
+ set(:project) { create(:project_empty_repo, :internal) }
+ set(:issue1) { create(:issue, project: project) }
+ set(:issue2) { create(:issue, project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
index 07b8c1ef479..bf95dbb7d09 100644
--- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -1,72 +1,115 @@
require 'spec_helper'
describe 'User views open merge requests' do
- let(:project) { create(:project, :public, :repository) }
+ set(:user) { create(:user) }
- context "when the target branch is the project's default branch" do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
-
- before do
- visit(project_merge_requests_path(project))
+ shared_examples_for 'shows merge requests' do
+ it 'shows merge requests' do
+ expect(page).to have_content(project.name).and have_content(merge_request.source_project.name)
end
+ end
- it 'shows open merge requests' do
- expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
- end
+ context 'when project is public' do
+ set(:project) { create(:project, :public, :repository) }
- it 'does not show target branch name' do
- expect(page).to have_content(merge_request.title)
- expect(find('.issuable-info')).not_to have_content(project.default_branch)
- end
- end
+ context 'when not signed in' do
+ context "when the target branch is the project's default branch" do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
- context "when the target branch is different from the project's default branch" do
- let!(:merge_request) do
- create(:merge_request,
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'feature_conflict')
- end
+ before do
+ visit(project_merge_requests_path(project))
+ end
- before do
- visit(project_merge_requests_path(project))
- end
+ include_examples 'shows merge requests'
- it 'shows target branch name' do
- expect(page).to have_content(merge_request.target_branch)
- end
- end
+ it 'shows open merge requests' do
+ expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
+ end
- context 'when a merge request has pipelines' do
- let!(:build) { create :ci_build, pipeline: pipeline }
+ it 'does not show target branch name' do
+ expect(page).to have_content(merge_request.title)
+ expect(find('.issuable-info')).not_to have_content(project.default_branch)
+ end
+ end
- let(:merge_request) do
- create(:merge_request_with_diffs,
- source_project: project,
- target_project: project,
- source_branch: 'merge-test')
- end
+ context "when the target branch is different from the project's default branch" do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'feature_conflict')
+ end
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows target branch name' do
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
+ context 'when a merge request has pipelines' do
+ let!(:build) { create :ci_build, pipeline: pipeline }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request)
+ end
+
+ before do
+ project.enable_ci
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows pipeline status' do
+ page.within('.mr-list') do
+ expect(page).to have_link('Pipeline: pending')
+ end
+ end
+ end
end
- before do
- project.enable_ci
+ context 'when signed in' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
- visit(project_merge_requests_path(project))
+ visit(project_merge_requests_path(project))
+ end
+
+ include_examples 'shows merge requests'
end
+ end
- it 'shows pipeline status' do
- page.within('.mr-list') do
- expect(page).to have_link('Pipeline: pending')
+ context 'when project is internal' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ set(:project) { create(:project, :internal, :repository) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_merge_requests_path(project))
end
+
+ include_examples 'shows merge requests'
end
end
end
diff --git a/spec/features/projects/user_views_details_spec.rb b/spec/features/projects/user_views_details_spec.rb
new file mode 100644
index 00000000000..ffc063654cd
--- /dev/null
+++ b/spec/features/projects/user_views_details_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+describe 'User views details' do
+ set(:user) { create(:user) }
+
+ shared_examples_for 'redirects to the sign in page' do
+ it 'redirects to the sign in page' do
+ expect(current_path).to eq(new_user_session_path)
+ end
+ end
+
+ shared_examples_for 'shows details of empty project' do
+ let(:user_has_ssh_key) { false }
+
+ it 'shows details' do
+ expect(page).not_to have_content('Git global setup')
+
+ page.all(:css, '.git-empty .clone').each do |element|
+ expect(element.text).to include(project.http_url_to_repo)
+ end
+
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ end
+ end
+
+ shared_examples_for 'shows details of non empty project' do
+ let(:user_has_ssh_key) { false }
+
+ it 'shows details' do
+ page.within('.breadcrumbs .breadcrumb-item-text') do
+ expect(page).to have_content(project.title)
+ end
+
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ end
+ end
+
+ context 'when project is public' do
+ context 'when project is empty' do
+ set(:project) { create(:project_empty_repo, :public) }
+
+ context 'when not signed in' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when user does not have ssh keys' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project'
+ end
+
+ context 'when user has ssh keys' do
+ before do
+ create(:personal_key, user: user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project' do
+ let(:user_has_ssh_key) { true }
+ end
+ end
+ end
+ end
+
+ context 'when project is not empty' do
+ set(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ context 'when not signed in' do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:host).and_return('www.example.com')
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when user does not have ssh keys' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+
+ context 'when user has ssh keys' do
+ before do
+ create(:personal_key, user: user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project' do
+ let(:user_has_ssh_key) { true }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when project is internal' do
+ set(:project) { create(:project, :internal, :repository) }
+
+ context 'when not signed in' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'redirects to the sign in page'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+ end
+
+ context 'when project is private' do
+ set(:project) { create(:project, :private) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'redirects to the sign in page'
+ end
+end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index a22b71fd1dc..268b5b83b73 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -28,7 +28,7 @@ import '~/lib/utils/common_utils';
preloadFixtures('merge_requests/diff_comment.html.raw');
beforeEach(function(done) {
loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
loadAwardsHandler(true).then((obj) => {
awardsHandler = obj;
spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
@@ -55,6 +55,9 @@ import '~/lib/utils/common_utils';
// restore original url root value
gon.relative_url_root = urlRoot;
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+
awardsHandler.destroy();
});
describe('::showEmojiMenu', function() {
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index f62bf43adb9..d5300d9c63d 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -19,6 +19,11 @@ describe('Quick Submit behavior', () => {
this.textarea = $('.js-quick-submit textarea').first();
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
it('does not respond to other keyCodes', () => {
this.textarea.trigger(keydownEvent({
keyCode: 32,
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index fa24aa426b6..2779686a6f5 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -1,110 +1,108 @@
/* eslint-disable space-before-function-paren, arrow-body-style */
-import '~/gl_field_errors';
+import GlFieldErrors from '~/gl_field_errors';
-((global) => {
+describe('GL Style Field Errors', function() {
preloadFixtures('static/gl_field_errors.html.raw');
- describe('GL Style Field Errors', function() {
- beforeEach(function() {
- loadFixtures('static/gl_field_errors.html.raw');
- const $form = this.$form = $('form.gl-show-field-errors');
- this.fieldErrors = new global.GlFieldErrors($form);
- });
+ beforeEach(function() {
+ loadFixtures('static/gl_field_errors.html.raw');
+ const $form = this.$form = $('form.gl-show-field-errors');
+ this.fieldErrors = new GlFieldErrors($form);
+ });
- it('should select the correct input elements', function() {
- expect(this.$form).toBeDefined();
- expect(this.$form.length).toBe(1);
- expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
- expect(inputs.length).toBe(4);
- });
+ it('should select the correct input elements', function() {
+ expect(this.$form).toBeDefined();
+ expect(this.$form.length).toBe(1);
+ expect(this.fieldErrors).toBeDefined();
+ const inputs = this.fieldErrors.state.inputs;
+ expect(inputs.length).toBe(4);
+ });
- it('should ignore elements with custom error handling', function() {
- const customErrorFlag = 'gl-field-error-ignore';
- const customErrorElem = $(`.${customErrorFlag}`);
+ it('should ignore elements with custom error handling', function() {
+ const customErrorFlag = 'gl-field-error-ignore';
+ const customErrorElem = $(`.${customErrorFlag}`);
- expect(customErrorElem.length).toBe(1);
+ expect(customErrorElem.length).toBe(1);
- const customErrors = this.fieldErrors.state.inputs.filter((input) => {
- return input.inputElement.hasClass(customErrorFlag);
- });
- expect(customErrors.length).toBe(0);
+ const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+ return input.inputElement.hasClass(customErrorFlag);
});
+ expect(customErrors.length).toBe(0);
+ });
- it('should not show any errors before submit attempt', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
+ it('should not show any errors before submit attempt', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(0);
- });
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(0);
+ });
- it('should show errors when input valid is submitted', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
+ it('should show errors when input valid is submitted', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
- this.$form.submit();
+ this.$form.submit();
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(4);
- });
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(4);
+ });
- it('should properly track validity state on input after invalid submission attempt', function() {
- this.$form.submit();
-
- const emailInputModel = this.fieldErrors.state.inputs[1];
- const fieldState = emailInputModel.state;
- const emailInputElement = emailInputModel.inputElement;
-
- // No input
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then empty input
- emailInputElement.val('').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
- });
+ it('should properly track validity state on input after invalid submission attempt', function() {
+ this.$form.submit();
+
+ const emailInputModel = this.fieldErrors.state.inputs[1];
+ const fieldState = emailInputModel.state;
+ const emailInputElement = emailInputModel.inputElement;
+
+ // No input
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then empty input
+ emailInputElement.val('').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+ });
- it('should properly infer error messages', function() {
- this.$form.submit();
- const trackedInputs = this.fieldErrors.state.inputs;
- const inputHasTitle = trackedInputs[1];
- const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
- const inputNoTitle = trackedInputs[2];
- const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+ it('should properly infer error messages', function() {
+ this.$form.submit();
+ const trackedInputs = this.fieldErrors.state.inputs;
+ const inputHasTitle = trackedInputs[1];
+ const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+ const inputNoTitle = trackedInputs[2];
+ const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
- expect(noTitleErrorElem.text()).toBe('This field is required.');
- expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
- });
+ expect(noTitleErrorElem.text()).toBe('This field is required.');
+ expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
index 837feacec1d..124fc030774 100644
--- a/spec/javascripts/gl_form_spec.js
+++ b/spec/javascripts/gl_form_spec.js
@@ -1,18 +1,11 @@
import autosize from 'vendor/autosize';
-import '~/gl_form';
+import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
window.autosize = autosize;
describe('GLForm', () => {
- const global = window.gl || (window.gl = {});
- const GLForm = global.GLForm;
-
- it('should be defined in the global scope', () => {
- expect(GLForm).toBeDefined();
- });
-
describe('when instantiated', function () {
beforeEach((done) => {
this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index 395dc560671..ac6ace48108 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -23,12 +23,17 @@ describe('Merge request notes', () => {
loadFixtures(discussionTabFixture);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
describe('up arrow', () => {
it('edits last comment when triggered in main form', () => {
const upArrowEvent = $.Event('keydown');
@@ -71,12 +76,17 @@ describe('Merge request notes', () => {
<textarea class="js-note-text"></textarea>
</form>`;
setFixtures(diffsResponse.html + noteFormHtml);
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
describe('up arrow', () => {
it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown');
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index ccdbfcba692..18916c5aa97 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -277,7 +277,7 @@ import 'vendor/jquery.scrollTo';
describe('loadDiff', function () {
beforeEach(() => {
loadFixtures('merge_requests/diff_comment.html.raw');
- spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gl.ImageFile = () => {};
window.notes = new Notes('', []);
spyOn(window.notes, 'toggleDiffNote').and.callThrough();
@@ -286,6 +286,9 @@ import 'vendor/jquery.scrollTo';
afterEach(() => {
delete window.gl.ImageFile;
delete window.notes;
+
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
});
it('requires an absolute pathname', function () {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 65d2e8fd9fb..66c52611614 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -39,7 +39,12 @@ import '~/notes';
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
- $('body').data('page', 'projects:merge_requets:show');
+ $('body').attr('data-page', 'projects:merge_requets:show');
+ });
+
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
});
describe('task lists', function() {
@@ -426,19 +431,17 @@ import '~/notes';
});
describe('putEditFormInPlace', () => {
- it('should call gl.GLForm with GFM parameter passed through', () => {
- spyOn(gl, 'GLForm');
-
- const $el = jasmine.createSpyObj('$form', ['find', 'closest']);
- $el.find.and.returnValue($('<div>'));
- $el.closest.and.returnValue($('<div>'));
+ it('should call GLForm with GFM parameter passed through', () => {
+ const notes = new Notes('', []);
+ const $el = $(`
+ <div>
+ <form></form>
+ </div>
+ `);
- Notes.prototype.putEditFormInPlace.call({
- getEditFormSelector: () => '',
- enableGFM: true
- }, $el);
+ notes.putEditFormInPlace($el);
- expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true);
+ expect(notes.glForm.enableGFM).toBeTruthy();
});
});
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
index 2b3a821dbd9..b24567ffc0c 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
@@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => {
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
+ expect($.ajax).toHaveBeenCalledWith({
+ url: prometheusMetrics.activeMetricsEndpoint,
+ dataType: 'json',
+ global: false,
+ });
deferred.resolve({ data: metrics, success: true });
@@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => {
it('should show empty state if response failed to load', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
@@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => {
it('should populate metrics list once response is loaded', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index a53f58b5d0d..cf811af3d6c 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils';
import 'vendor/fuzzaldrin-plus';
(function() {
- var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+ var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
var userName = 'root';
widget = null;
@@ -29,25 +29,31 @@ import 'vendor/fuzzaldrin-plus';
groupName = 'Gitlab Org';
+ const removeBodyAttributes = function() {
+ const $body = $('body');
+
+ $body.removeAttr('data-page');
+ $body.removeAttr('data-project');
+ $body.removeAttr('data-group');
+ };
+
// Add required attributes to body before starting the test.
// section would be dashboard|group|project
- addBodyAttributes = function(section) {
- var $body;
+ const addBodyAttributes = function(section) {
if (section == null) {
section = 'dashboard';
}
- $body = $('body');
- $body.removeAttr('data-page');
- $body.removeAttr('data-project');
- $body.removeAttr('data-group');
+
+ const $body = $('body');
+ removeBodyAttributes();
switch (section) {
case 'dashboard':
- return $body.data('page', 'root:index');
+ return $body.attr('data-page', 'root:index');
case 'group':
- $body.data('page', 'groups:show');
+ $body.attr('data-page', 'groups:show');
return $body.data('group', 'gitlab-org');
case 'project':
- $body.data('page', 'projects:show');
+ $body.attr('data-page', 'projects:show');
return $body.data('project', 'gitlab-ce');
}
};
@@ -108,7 +114,7 @@ import 'vendor/fuzzaldrin-plus';
preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
- widget = new gl.SearchAutocomplete;
+
// Prevent turbolinks from triggering within gl_dropdown
spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
@@ -120,6 +126,8 @@ import 'vendor/fuzzaldrin-plus';
});
afterEach(function() {
+ // Undo what we did to the shared <body>
+ removeBodyAttributes();
window.gon = {};
});
it('should show Dashboard specific dropdown menu', function() {
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 695fd6f8573..8e524f9b05a 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -18,6 +18,10 @@ describe Gitlab::FileDetector do
expect(described_class.type_of('README.md')).to eq(:readme)
end
+ it 'returns nil for a README file in a directory' do
+ expect(described_class.type_of('foo/README.md')).to be_nil
+ end
+
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
@@ -52,6 +56,14 @@ describe Gitlab::FileDetector do
end
end
+ it 'returns the type of an issue template' do
+ expect(described_class.type_of('.gitlab/issue_templates/foo.md')).to eq(:issue_template)
+ end
+
+ it 'returns the type of a merge request template' do
+ expect(described_class.type_of('.gitlab/merge_request_templates/foo.md')).to eq(:merge_request_template)
+ end
+
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index b82e5e6d000..b11fa38856b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1510,6 +1510,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#fetch' do
+ let(:git_path) { Gitlab.config.git.bin_path }
+ let(:remote_name) { 'my_remote' }
+
+ subject { repository.fetch(remote_name) }
+
+ it 'fetches the remote and returns true if the command was successful' do
+ expect(repository).to receive(:popen)
+ .with(%W(#{git_path} fetch #{remote_name}), repository.path)
+ .and_return(['', 0])
+
+ expect(subject).to be(true)
+ end
+ end
+
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json
new file mode 100644
index 00000000000..82a1fbd2fc5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.group.json
@@ -0,0 +1,188 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "visibility_level": 10,
+ "archived": false,
+ "milestones": [
+ {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ ],
+ "labels": [
+ {
+ "id": 2,
+ "title": "project label",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
+ }
+ ],
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 1,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 6,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "group label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "GroupLabel",
+ "priorities": []
+ }
+ },
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 2,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 2,
+ "title": "A group milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 100
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 2,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "hooks": [
+
+ ]
+}
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index 2d8f3d4a566..02450478a77 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -5,9 +5,9 @@
"milestones": [
{
"id": 1,
- "title": "test milestone",
+ "title": "Project milestone",
"project_id": 8,
- "description": "test milestone",
+ "description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
@@ -19,7 +19,7 @@
"labels": [
{
"id": 2,
- "title": "test2",
+ "title": "A project label",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
@@ -63,30 +63,21 @@
"last_edited_at": null,
"last_edited_by_id": null,
"group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
"label_links": [
{
"id": 11,
- "label_id": 6,
- "target_id": 1,
- "target_type": "Issue",
- "created_at": "2017-08-15T18:37:40.795Z",
- "updated_at": "2017-08-15T18:37:40.795Z",
- "label": {
- "id": 6,
- "title": "group label",
- "color": "#A8D695",
- "project_id": null,
- "created_at": "2017-08-15T18:37:19.698Z",
- "updated_at": "2017-08-15T18:37:19.698Z",
- "template": false,
- "description": "",
- "group_id": 5,
- "type": "GroupLabel",
- "priorities": []
- }
- },
- {
- "id": 11,
"label_id": 2,
"target_id": 1,
"target_type": "Issue",
@@ -94,14 +85,14 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "project label",
+ "title": "Another project label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
"updated_at": "2017-08-15T18:37:19.698Z",
"template": false,
"description": "",
- "group_id": 5,
+ "group_id": null,
"type": "ProjectLabel",
"priorities": []
}
@@ -109,10 +100,6 @@
]
}
],
- "snippets": [
-
- ],
- "hooks": [
-
- ]
+ "snippets": [],
+ "hooks": []
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index efe11ca794a..4301eee17dc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'JSON' do
it 'restores models based on JSON' do
- expect(@restored_project_json).to be true
+ expect(@restored_project_json).to be_truthy
end
it 'restore correct project features' do
@@ -182,6 +182,53 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
+ shared_examples 'restores project successfully' do
+ it 'correctly restores project' do
+ expect(shared.errors).to be_empty
+ expect(restored_project_json).to be_truthy
+ end
+ end
+
+ shared_examples 'restores project correctly' do |**results|
+ it 'has labels' do
+ expect(project.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has label priorities' do
+ expect(project.labels.first.priorities).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issues' do
+ expect(project.issues.size).to eq(results.fetch(:issues, 0))
+ end
+
+ it 'has issue with group label and project label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
+ shared_examples 'restores group correctly' do |**results|
+ it 'has group label' do
+ expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has group milestone' do
+ expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issue with group label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
context 'Light JSON' do
let(:user) { create(:user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
@@ -190,33 +237,45 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:restored_project_json) { project_tree_restorer.restore }
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
-
allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
end
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
+ context 'with a simple project' do
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+
+ restored_project_json
+ end
+
+ it_behaves_like 'restores project correctly',
+ issues: 1,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
- restored_project_json
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
- expect(shared.errors.first).to be_nil
+ restored_project_json
+
+ expect(shared.errors).to be_empty
+ end
end
end
- end
- context 'when there is an existing build with build token' do
- it 'restores project json correctly' do
- create(:ci_build, token: 'abcd')
+ context 'when there is an existing build with build token' do
+ before do
+ create(:ci_build, token: 'abcd')
+ end
- expect(restored_project_json).to be true
+ it_behaves_like 'restores project successfully'
end
end
- context 'with group' do
+ context 'with a project that has a group' do
let!(:project) do
create(:project,
:builds_disabled,
@@ -227,43 +286,22 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json")
restored_project_json
end
- it 'correctly restores project' do
- expect(restored_project_json).to be_truthy
- expect(shared.errors).to be_empty
- end
+ it_behaves_like 'restores project successfully'
+ it_behaves_like 'restores project correctly',
+ issues: 2,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
- it 'has labels' do
- expect(project.labels.count).to eq(2)
- end
-
- it 'creates group label' do
- expect(project.group.labels.count).to eq(1)
- end
-
- it 'has label priorities' do
- expect(project.labels.first.priorities).not_to be_empty
- end
-
- it 'has milestones' do
- expect(project.milestones.count).to eq(1)
- end
-
- it 'has issue' do
- expect(project.issues.count).to eq(1)
- expect(project.issues.first.labels.count).to eq(2)
- end
-
- it 'has issue with group label and project label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "GroupLabel").count).to eq(1)
- expect(labels.where(type: "ProjectLabel").count).to eq(1)
- end
+ it_behaves_like 'restores group correctly',
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 5d78aed5b4f..f44693a71bb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1509,7 +1509,9 @@ describe Repository do
:gitignore,
:koding,
:gitlab_ci,
- :avatar
+ :avatar,
+ :issue_template,
+ :merge_request_template
])
repository.after_change_head
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 862920ad7c3..9f3b5a809d7 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -222,13 +222,6 @@ describe API::Helpers do
expect { current_user }.to raise_error /401/
end
- it "returns a 401 response for a token without the appropriate scope" do
- personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
-
- expect { current_user }.to raise_error /401/
- end
-
it "leaves user as is when sudo not specified" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
@@ -238,18 +231,25 @@ describe API::Helpers do
expect(current_user).to eq(user)
end
+ it "does not allow tokens without the appropriate scope" do
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
+ end
+
it 'does not allow revoked tokens' do
personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error API::APIGuard::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error API::APIGuard::ExpiredError
end
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 9c9b0c4c4a1..a1f7dc44d31 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -6,11 +6,7 @@ describe MergeRequests::Conflicts::ResolveService do
let(:project) { create(:project, :public, :repository) }
let(:forked_project) do
- forked_project = fork_project(project, user)
- TestEnv.copy_repo(forked_project,
- bare_repo: TestEnv.forked_repo_path_bare,
- refs: TestEnv::FORKED_BRANCH_SHA)
- forked_project
+ fork_project_with_submodules(project, user)
end
let(:merge_request) do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 80213d093f1..d1043f99b5a 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -185,7 +185,7 @@ describe MergeRequests::MergeService do
context 'source branch removal' do
context 'when the source branch is protected' do
let(:service) do
- described_class.new(project, user, should_remove_source_branch: '1')
+ described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
@@ -200,7 +200,7 @@ describe MergeRequests::MergeService do
context 'when the source branch is the default branch' do
let(:service) do
- described_class.new(project, user, should_remove_source_branch: '1')
+ described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
@@ -215,10 +215,10 @@ describe MergeRequests::MergeService do
context 'when the source branch can be removed' do
context 'when MR author set the source branch to be removed' do
- let(:service) do
- merge_request.merge_params['force_remove_source_branch'] = '1'
- merge_request.save!
- described_class.new(project, user, commit_message: 'Awesome message')
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' })
end
it 'removes the source branch using the author user' do
@@ -227,11 +227,20 @@ describe MergeRequests::MergeService do
.and_call_original
service.execute(merge_request)
end
+
+ context 'when the merger set the source branch not to be removed' do
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => false) }
+
+ it 'does not delete the source branch' do
+ expect(DeleteBranchService).not_to receive(:new)
+ service.execute(merge_request)
+ end
+ end
end
context 'when MR merger set the source branch to be removed' do
let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1')
+ described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => true)
end
it 'removes the source branch using the current user' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b64ca5be8fc..b13e12e7c94 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -731,6 +731,18 @@ describe NotificationService, :mailer do
should_not_email(@u_participating)
end
+ it "doesn't send multiple email when a user is subscribed to multiple given labels" do
+ subscriber_to_both = create(:user) do |user|
+ [label_1, label_2].each { |label| label.toggle_subscription(user, project) }
+ end
+
+ notification.relabeled_issue(issue, [label_1, label_2], @u_disabled)
+
+ should_email(subscriber_to_label_1)
+ should_email(subscriber_to_label_2)
+ should_email(subscriber_to_both)
+ end
+
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb
index 57e28e040d7..111534f2f26 100644
--- a/spec/support/api/scopes/read_user_shared_examples.rb
+++ b/spec/support/api/scopes/read_user_shared_examples.rb
@@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do
stub_container_registry_config(enabled: true)
end
- it 'returns a "401" response' do
+ it 'returns a "403" response' do
get api_call.call(path, user, personal_access_token: token)
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(403)
end
end
end
@@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
- it 'returns a "401" response' do
+ it 'returns a "403" response' do
post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3)
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(403)
end
end
end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 3e979f2f470..b39052923dd 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,6 +1,6 @@
module EmailHelpers
- def sent_to_user?(user, recipients = email_recipients)
- recipients.include?(user.notification_email)
+ def sent_to_user(user, recipients: email_recipients)
+ recipients.count { |to| to == user.notification_email }
end
def reset_delivered_emails!
@@ -10,17 +10,17 @@ module EmailHelpers
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
- users.each { |user| should_email(user, recipients) }
+ users.each { |user| should_email(user, recipients: recipients) }
expect(recipients.count).to eq(users.count)
end
- def should_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_truthy
+ def should_email(user, times: 1, recipients: email_recipients)
+ expect(sent_to_user(user, recipients: recipients)).to eq(times)
end
- def should_not_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_falsey
+ def should_not_email(user, recipients: email_recipients)
+ should_email(user, times: 0, recipients: recipients)
end
def should_not_email_anyone
diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb
index 0d1c6792d13..d6680735aa1 100644
--- a/spec/support/project_forks_helper.rb
+++ b/spec/support/project_forks_helper.rb
@@ -52,7 +52,7 @@ module ProjectForksHelper
TestEnv.copy_repo(forked_project,
bare_repo: TestEnv.forked_repo_path_bare,
refs: TestEnv::FORKED_BRANCH_SHA)
-
+ forked_project.repository.after_import
forked_project
end
end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 520a86352f7..c79ba5080a3 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -41,7 +41,8 @@ captures/
.idea/libraries
# Keystore files
-*.jks
+# Uncomment the following line if you do not want to check your keystore files in.
+#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index e3923f96fce..ffa6ecc3f9b 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -31,3 +31,12 @@ Makefile.in
# http://www.gnu.org/software/texinfo
/texinfo.tex
+
+# http://www.gnu.org/software/m4/
+
+m4/libtool.m4
+m4/ltoptions.m4
+m4/ltsugar.m4
+m4/ltversion.m4
+m4/lt~obsolete.m4
+autom4te.cache
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index ac67aaf3243..b6d65867dac 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -1,6 +1,8 @@
/_build
/cover
/deps
+/doc
+/.fetch
erl_crash.dump
*.ez
*.beam
diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore
index c92aea0fe0c..ab97a8cc3e1 100644
--- a/vendor/gitignore/ExtJs.gitignore
+++ b/vendor/gitignore/ExtJs.gitignore
@@ -10,3 +10,5 @@ ext/
modern.json
modern.jsonp
resources/sass/.sass-cache/
+resources/.arch-internal-preview.css
+.arch-internal-preview.css
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index 09dfde64b5f..cca150a88dd 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -19,4 +19,4 @@ slprj/
octave-workspace
# Simulink autosave extension
-.autosave
+*.autosave
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index 37de8bb4793..cd0c7d3e45a 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,11 +2,17 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## Build generated
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
-
-## Various settings
+*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -15,9 +21,3 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
-xcuserdata/
-
-## Other
-*.moved-aside
-*.xccheckout
-*.xcscmblueprint
diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore
index 9d1061e8bc4..135767fc075 100644
--- a/vendor/gitignore/Global/macOS.gitignore
+++ b/vendor/gitignore/Global/macOS.gitignore
@@ -1,5 +1,5 @@
# General
-*.DS_Store
+.DS_Store
.AppleDouble
.LSOverride
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 53a74e74657..b6bf3a9c96a 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -251,7 +251,7 @@
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
/administrator/language/overrides/*
-/administrator/logs/index.html
+/administrator/logs/*
/administrator/manifests/*
/administrator/modules/mod_custom/*
/administrator/modules/mod_feed/*
diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore
index f7817ae5c36..da0b20424a0 100644
--- a/vendor/gitignore/OCaml.gitignore
+++ b/vendor/gitignore/OCaml.gitignore
@@ -18,3 +18,6 @@ _build/
# oasis generated files
setup.data
setup.log
+
+# Merlin configuring file for Vim and Emacs
+.merlin
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 113294a5f18..af2f537516d 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -23,6 +23,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
+MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
@@ -51,6 +52,8 @@ coverage.xml
# Django stuff:
*.log
+.static_storage/
+.media/
local_settings.py
# Flask stuff:
@@ -84,6 +87,8 @@ celerybeat-schedule
env/
venv/
ENV/
+env.bak/
+venv.bak/
# Spyder project settings
.spyderproject
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
index fe67fdf1ee6..037a1e75790 100644
--- a/vendor/gitignore/Qt.gitignore
+++ b/vendor/gitignore/Qt.gitignore
@@ -31,11 +31,9 @@ ui_*.h
Makefile*
*build-*
-
# Qt unit tests
target_wrapper.*
-
# QtCreator
*.autosave
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index a0322dbd35a..b6418e51766 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -13,6 +13,7 @@
## Intermediate documents:
*.dvi
+*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index f20453be963..9b5aebb1b35 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -5,3 +5,6 @@
# Module directory
.terraform/
+
+# Variable values for development
+terraform.tfvars
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
index ea05e1fb2a9..b6b0743f62a 100644
--- a/vendor/gitignore/Umbraco.gitignore
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -1,3 +1,7 @@
+## Ignore Umbraco files/folders generated for each instance
+##
+## Get latest from https://github.com/github/gitignore/blob/master/Umbraco.gitignore
+
# Note: VisualStudio gitignore rules may also be relevant
# Umbraco
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index f652b45c2ee..0867ec5a7ee 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -96,6 +96,9 @@ ipch/
*.vspx
*.sap
+# Visual Studio Trace Files
+*.e2e
+
# TFS 2012 Local Workspace
$tf/
@@ -297,3 +300,6 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore
index 80adb154900..f0b7d8585b7 100644
--- a/vendor/gitignore/ZendFramework.gitignore
+++ b/vendor/gitignore/ZendFramework.gitignore
@@ -19,7 +19,6 @@ temp/
data/DoctrineORMModule/Proxy/
data/DoctrineORMModule/cache/
-
# Legacy ZF1
demos/
extras/documentation
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
index 8a214352d2a..86e4985d8d2 100644
--- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -29,7 +29,7 @@ format:
compile:
stage: build
script:
- - go build -race -ldflags "-extldflags '-static'" -o mybinary
+ - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
artifacts:
paths:
- mybinary
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index 91b096654d1..ba2efbd03a0 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -7,8 +7,8 @@
# This template will build and test your projects as well as create the documentation.
#
# * Caches downloaded dependencies and plugins between invocation.
-# * Does only verify merge requests but deploy built artifacts of the
-# master branch.
+# * Verify but don't deploy merge requests.
+# * Deploy built artifacts from master branch only.
# * Shows how to use multiple jobs in test stage for verifying functionality
# with multiple JDKs.
# * Uses site:stage to collect the documentation for multi-module projects.
@@ -20,7 +20,7 @@ variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
- # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
+ # `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
@@ -100,4 +100,3 @@ pages:
- public
only:
- master
-
diff --git a/vendor/gitlab-ci-yml/Python.gitlab-ci.yml b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
new file mode 100644
index 00000000000..a2882a5407d
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# This file is a template, and might need editing before it works on your project.
+image: python:latest
+
+before_script:
+ - python -V # Print out python version for debugging
+
+test:
+ script:
+ - python setup.py test
+ - pip install tox flake8 # you can also use tox
+ - tox -e py36,flake8
+
+run:
+ script:
+ - python setup.py bdist_wheel
+ # an alternative approach is to install and run:
+ - pip install dist/*
+ # run the command here
+ artifacts:
+ paths:
+ - dist/*.whl
+
+pages:
+ script:
+ - pip install sphinx sphinx-rtd-theme
+ - cd doc ; make html
+ - mv build/html/ ../public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master