summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/dispatcher.js.es68
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es65
-rw-r--r--app/assets/javascripts/gl_dropdown.js26
-rw-r--r--app/assets/javascripts/issuable.js.es64
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es692
-rw-r--r--app/assets/javascripts/labels_select.js146
-rw-r--r--app/assets/javascripts/render_math.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/pages/editor.scss49
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/models/ci/build.rb20
-rw-r--r--app/models/ci/pipeline.rb18
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/environment.rb57
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/namespace.rb13
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb15
-rw-r--r--app/services/ci/image_for_build_service.rb8
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml6
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--changelogs/unreleased/22864-add-environment-slug.yml4
-rw-r--r--changelogs/unreleased/24824-dropdown-items-focus.yml4
-rw-r--r--changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml4
-rw-r--r--changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml4
-rw-r--r--changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml4
-rw-r--r--changelogs/unreleased/25617-todos-filter-placeholder.yml4
-rw-r--r--changelogs/unreleased/file-template-dropwdown-proper-position.yml4
-rw-r--r--changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml4
-rw-r--r--changelogs/unreleased/show-commit-status-from-latest-pipeline.yml4
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb53
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb18
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb60
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb15
-rw-r--r--db/schema.rb16
-rw-r--r--doc/administration/auth/README.md3
-rw-r--r--doc/administration/auth/img/okta_admin_panel.pngbin0 -> 26164 bytes
-rw-r--r--doc/administration/auth/img/okta_saml_settings.pngbin0 -> 25470 bytes
-rw-r--r--doc/administration/auth/okta.md160
-rw-r--r--doc/administration/custom_hooks.md22
-rw-r--r--doc/administration/high_availability/database.md6
-rw-r--r--doc/administration/high_availability/redis.md8
-rw-r--r--doc/api/enviroments.md8
-rw-r--r--doc/ci/environments.md34
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md22
-rw-r--r--doc/development/ux_guide/basics.md16
-rw-r--r--doc/development/ux_guide/img/cursors-default.pngbin0 -> 567 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-ibeam.pngbin0 -> 383 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-move.pngbin0 -> 276 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panclosed.pngbin0 -> 483 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panopened.pngbin0 -> 622 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-pointer.pngbin0 -> 574 bytes
-rw-r--r--doc/project_services/mattermost_slash_commands.md18
-rw-r--r--doc/update/8.14-to-8.15.md2
-rw-r--r--features/admin/active_tab.feature54
-rw-r--r--features/steps/admin/active_tab.rb41
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/api/helpers/custom_validators.rb14
-rw-r--r--lib/gitlab/asciidoc.rb4
-rw-r--r--lib/gitlab/badge/build/status.rb4
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb2
-rw-r--r--lib/gitlab/regex.rb9
-rw-r--r--public/404.html9
-rw-r--r--public/422.html9
-rw-r--r--public/500.html9
-rw-r--r--public/502.html9
-rw-r--r--public/503.html9
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb90
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb42
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb8
-rw-r--r--spec/javascripts/extensions/object_spec.js.es625
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb4
-rw-r--r--spec/lib/gitlab/regex_spec.rb16
-rw-r--r--spec/models/build_spec.rb36
-rw-r--r--spec/models/ci/pipeline_spec.rb59
-rw-r--r--spec/models/commit_spec.rb29
-rw-r--r--spec/models/environment_spec.rb48
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb22
-rw-r--r--spec/requests/api/environments_spec.rb17
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb17
90 files changed, 1281 insertions, 354 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index c4e41f94594..ee74734aa22 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.0.3
+4.1.0
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 7d588e8eee6..1e259a16f06 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -74,7 +74,9 @@
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new gl.IssuableBulkActions();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+ });
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -144,10 +146,6 @@
new ZenMode();
new MergedButtons();
break;
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
new gl.Activities();
break;
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 00000000000..70a2d765abd
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-restricted-syntax */
+
+// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+if (typeof Object.assign !== 'function') {
+ Object.assign = function assign(target, ...args) {
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ const to = Object(target);
+
+ for (let index = 0; index < args.length; index += 1) {
+ const nextSource = args[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (const nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 245383438d1..90010b9fd54 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -67,14 +67,15 @@
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- var _a, _y, regexp, match;
+ var _a, _y, regexp, match, atSymbols;
+ atSymbols = Object.keys(this.app.controllers).join('|');
subtext = subtext.split(' ').pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi');
+ regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
match = regexp.exec(subtext);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 68a345d83f9..57dabfe05e4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -23,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
@@ -348,12 +347,12 @@
$el = $(this);
selected = self.rowClicked($el);
if (self.options.clicked) {
- self.options.clicked(selected, $el, e);
+ self.options.clicked(selected[0], $el, e, selected[1]);
}
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
- self.updateLabel(selected, $el, self);
+ self.updateLabel(selected[0], $el, self);
}
$el.trigger('blur');
@@ -444,12 +443,6 @@
this.resetRows();
this.addArrowKeyEvent();
- if (this.options.setIndeterminateIds) {
- this.options.setIndeterminateIds.call(this);
- }
- if (this.options.setActiveIds) {
- this.options.setActiveIds.call(this);
- }
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
@@ -483,11 +476,6 @@
if (this.options.filterable) {
$input.blur().val("");
}
- // Triggering 'keyup' will re-render the dropdown which is not always required
- // specially if we want to keep the state of the dropdown needed for bulk-assignment
- if (!this.options.persistWhenHide) {
- $input.trigger("input");
- }
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
@@ -620,7 +608,8 @@
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -641,7 +630,7 @@
el.addClass(ACTIVE_CLASS);
}
- return selectedObject;
+ return [selectedObject];
}
field = [];
@@ -659,6 +648,7 @@
}
if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
@@ -668,6 +658,7 @@
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
@@ -677,6 +668,7 @@
this.addInput(fieldName, value, selectedObject);
}
} else {
+ isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
@@ -697,7 +689,7 @@
}
}
- return selectedObject;
+ return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index b174eb2ff96..1c10a7445bb 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -144,6 +144,9 @@
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
@@ -155,7 +158,6 @@
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
- this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index ad25104152c..52fd5d71b18 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -5,9 +5,10 @@
((global) => {
class IssuableBulkActions {
- constructor({ container, form, issues } = {}) {
- this.container = container || $('.content'),
+ constructor({ container, form, issues, prefixId } = {}) {
+ this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
+ this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
@@ -16,10 +17,6 @@
Issuable.initChecks();
}
- getElement(selector) {
- return this.container.find(selector);
- }
-
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
@@ -73,10 +70,7 @@
getUnmarkedIndeterminedLabels() {
const result = [];
- const labelsToKeep = [];
-
- this.getElement('.labels-filter .is-indeterminate')
- .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+ const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
@@ -106,45 +100,65 @@
}
};
if (this.willUpdateLabels) {
- this.getLabelsToApply().map(function(id) {
- return formData.update.add_label_ids.push(id);
- });
- this.getLabelsToRemove().map(function(id) {
- return formData.update.remove_label_ids.push(id);
- });
+ formData.update.add_label_ids = this.$labelDropdown.data('marked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
- getLabelsToApply() {
+ setOriginalDropdownData() {
+ const $labelSelect = $('.bulk-update .js-label-select');
+ $labelSelect.data('common', this.getOriginalCommonIds());
+ $labelSelect.data('marked', this.getOriginalMarkedIds());
+ $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
+ }
+
+ // From issuable's initial bulk selection
+ getOriginalCommonIds() {
const labelIds = [];
- const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
- $labels.each(function(k, label) {
- if (label) {
- return labelIds.push(parseInt($(label).val()));
- }
+
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return labelIds;
+ return _.intersection.apply(this, labelIds);
}
+ // From issuable's initial bulk selection
+ getOriginalMarkedIds() {
+ const labelIds = [];
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+ });
+ return _.intersection.apply(this, labelIds);
+ }
- /**
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- */
-
- getLabelsToRemove() {
- const result = [];
- const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- const labelsToApply = this.getLabelsToApply();
- indeterminatedLabels.map(function(id) {
- // We need to exclude label IDs that will be applied
- // By not doing this will cause issues from selection to not add labels at all
- if (labelsToApply.indexOf(id) === -1) {
- return result.push(id);
- }
+ // From issuable's initial bulk selection
+ getOriginalIndeterminateIds() {
+ const uniqueIds = [];
+ const labelIds = [];
+ let issuableLabels = [];
+
+ // Collect unique label IDs for all checked issues
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+ issuableLabels.forEach((labelId) => {
+ // Store unique IDs
+ if (uniqueIds.indexOf(labelId) === -1) {
+ uniqueIds.push(labelId);
+ }
+ });
+ // Store array of IDs per issuable
+ labelIds.push(issuableLabels);
});
- return result;
+ // Add uniqueIds to add it as argument for _.intersection
+ labelIds.unshift(uniqueIds);
+ // Return IDs that are present but not in all selected issueables
+ return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+ }
+
+ getElement(selector) {
+ this.scopeEl = this.scopeEl || $('.content');
+ return this.scopeEl.find(selector);
}
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d29a5edb9ad..ec2fc87bece 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -8,8 +8,9 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
@@ -125,7 +126,7 @@
});
});
};
- return $dropdown.glDropdown({
+ $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
@@ -172,33 +173,40 @@
});
},
renderRow: function(label, instance) {
- var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = instance.indeterminateIds;
- active = instance.activeIds;
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
- if (active.indexOf(label.id) !== -1) {
+
+ if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
- // Add input manually
- instance.addInput(this.fieldName, label.id);
}
- }
- if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
- selectedClass.push('is-active');
- }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
+ }
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
}
if (label.duplicate) {
spacing = 100 / label.color.length;
@@ -234,7 +242,6 @@
// Return generated html
return $li.html($a).prop('outerHTML');
},
- persistWhenHide: $dropdown.data('persistWhenHide'),
search: {
fields: ['title']
},
@@ -313,18 +320,15 @@
}
}
}
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- // If we are persisting state we need the classes
- if (!this.options.persistWhenHide) {
- return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
- }
- }
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(label, $el, e) {
+ clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page;
- _this.enableBulkLabelDropdown();
+
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
@@ -333,12 +337,11 @@
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -400,17 +403,10 @@
}
}
},
- setIndeterminateIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.indeterminateIds = _this.getIndeterminateIds();
- }
- },
- setActiveIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.activeIds = _this.getActiveIds();
- }
- }
});
+
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
@@ -423,34 +419,9 @@
if ($('.selected_issue:checked').length) {
return;
}
- // Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
- // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
- LabelsSelect.prototype.getIndeterminateIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.flatten(label_ids);
- };
-
- LabelsSelect.prototype.getActiveIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.intersection.apply(_, label_ids);
- };
-
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
@@ -459,8 +430,59 @@
}
};
- return LabelsSelect;
+ LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+ var issuableBulkActions = $('.bulk-update').data('bulkActions');
+
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
+ }
+
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
+ }
+
+ // If an indeterminate item is being unmarked
+ if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ };
+
+ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+ var labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ };
+
+ return LabelsSelect;
})();
}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index a8a56430f88..209e7a8661d 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ecd540793d0..11adf2568a1 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -188,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
- outline: 0;
}
&.dropdown-menu-empty-link {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 4b2e96dff8e..3fdb4f510fa 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -51,8 +51,16 @@
.new-file-name {
display: inline-block;
- width: 450px;
+ max-width: 450px;
float: left;
+
+ @media(max-width: $screen-md-max) {
+ width: 280px;
+ }
+
+ @media(max-width: $screen-sm-max) {
+ width: 180px;
+ }
}
.file-buttons {
@@ -114,3 +122,42 @@
}
}
}
+
+@media(max-width: $screen-xs-max){
+ .file-editor {
+ .file-title {
+ .pull-right {
+ height: auto;
+ }
+ }
+
+ .new-file-name {
+ max-width: none;
+ width: 100%;
+ margin-bottom: 3px;
+ }
+
+ .file-buttons {
+ display: block;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .soft-wrap-toggle {
+ width: 100%;
+ margin: 3px 0;
+ }
+
+ .encoding-selector,
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
+ display: block;
+ margin: 3px 0;
+
+ button {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index f6d4ea4659a..77dc9e7d538 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,11 +12,18 @@ module GroupsHelper
end
def group_title(group, name = nil, url = nil)
- full_title = link_to(simple_sanitize(group.name), group_path(group))
+ full_title = ''
+
+ group.parents.each do |parent|
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent))
+ full_title += ' / '.html_safe
+ end
+
+ full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do
- full_title
+ full_title.html_safe
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 9cda3b78761..d2177f683a1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -52,7 +52,7 @@ module ProjectsHelper
def project_title(project)
namespace_link =
if project.group
- link_to(simple_sanitize(project.group.name), group_path(project.group))
+ group_title(project.group)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
@@ -390,7 +390,7 @@ module ProjectsHelper
"success"
end
end
-
+
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 45a416a4c41..fdbf28a1d68 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,14 @@ module Ci
has_many :deployments, as: :deployable
+ # The "environment" field for builds is a String, and is the unexpanded name
+ def persisted_environment
+ @persisted_environment ||= Environment.find_by(
+ name: expanded_environment_name,
+ project_id: gl_project_id
+ )
+ end
+
serialize :options
serialize :yaml_variables
@@ -143,7 +151,7 @@ module Ci
end
def expanded_environment_name
- ExpandVariables.expand(environment, variables) if environment
+ ExpandVariables.expand(environment, simple_variables) if environment
end
def has_environment?
@@ -206,7 +214,8 @@ module Ci
slugified.gsub(/[^a-z0-9]/, '-')[0..62]
end
- def variables
+ # Variables whose value does not depend on other variables
+ def simple_variables
variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
@@ -219,6 +228,13 @@ module Ci
variables
end
+ # All variables, including those dependent on other variables
+ def variables
+ variables = simple_variables
+ variables += persisted_environment.predefined_variables if persisted_environment.present?
+ variables
+ end
+
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 54f73171fd4..48354cdbefb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -88,8 +88,24 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest, ->(ref = nil) do
+ max_id = unscope(:select)
+ .select("max(#{quoted_table_name}.id)")
+ .group(:ref, :sha)
+
+ if ref
+ where(id: max_id, ref: ref)
+ else
+ where(id: max_id)
+ end
+ end
+
+ def self.latest_status(ref = nil)
+ latest(ref).status
+ end
+
def self.latest_successful_for(ref)
- where(ref: ref).order(id: :desc).success.first
+ success.latest(ref).first
end
def self.truncate_sha(sha)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1831cc7e175..69cfc47f5bf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -228,13 +228,9 @@ class Commit
def status(ref = nil)
@statuses ||= {}
- if @statuses.key?(ref)
- @statuses[ref]
- elsif ref
- @statuses[ref] = pipelines.where(ref: ref).status
- else
- @statuses[ref] = pipelines.status
- end
+ return @statuses[ref] if @statuses.key?(ref)
+
+ @statuses[ref] = pipelines.latest_status(ref)
end
def revert_branch_name
@@ -270,7 +266,7 @@ class Commit
@merged_merge_request_hash ||= Hash.new do |hash, user|
hash[user] = merged_merge_request_no_cache(user)
end
-
+
@merged_merge_request_hash[current_user]
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 96700143ddd..8ef1c841ea3 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,9 +1,15 @@
class Environment < ActiveRecord::Base
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
belongs_to :project, required: true, validate: true
has_many :deployments
before_validation :nullify_external_url
+ before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+
before_save :set_environment_type
validates :name,
@@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :slug,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 24 },
+ format: { with: Gitlab::Regex.environment_slug_regex,
+ message: Gitlab::Regex.environment_slug_regex_message }
+
validates :external_url,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
@@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def predefined_variables
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+ ]
+ end
+
def recently_updated_on_branch?(ref)
ref.to_s == last_deployment.try(:ref)
end
@@ -107,4 +127,41 @@ class Environment < ActiveRecord::Base
action.expanded_environment_name == environment
end
end
+
+ # An environment name is not necessarily suitable for use in URLs, DNS
+ # or other third-party contexts, so provide a slugified version. A slug has
+ # the following properties:
+ # * contains only lowercase letters (a-z), numbers (0-9), and '-'
+ # * begins with a letter
+ # * has a maximum length of 24 bytes (OpenShift limitation)
+ # * cannot end with `-`
+ def generate_slug
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # Maximum length: 24 characters (OpenShift limitation)
+ slugified = slugified[0..23]
+
+ # Cannot end with a "-" character (Kubernetes label limitation)
+ slugified = slugified[0..-2] if slugified[-1] == "-"
+
+ # Add a random suffix, shortening the current string if necessary, if it
+ # has been slugified. This ensures uniqueness.
+ slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+ self.slug = slugified
+ end
+
+ private
+
+ # Slugifying a name may remove the uniqueness guarantee afforded by it being
+ # based on name (which must be unique). To compensate, we add a random
+ # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
+ # but the chance of collisions is vanishingly small
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 4248e1162d8..ac8a82c8c1e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -83,7 +83,7 @@ class Group < Namespace
end
def human_name
- name
+ full_name
end
def visibility_level_field
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index f0479d94986..fd42f2328d8 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base
end
end
+ def full_name
+ @full_name ||=
+ if parent
+ parent.full_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def parents
+ @parents ||= parent ? parent.parents + [parent] : []
+ end
+
private
def repository_storage_paths
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index 005014fa1de..b7da3f8e7eb 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,18 +10,29 @@ module Ci
end
end
+ def project
+ pipeline.project
+ end
+
private
def create_build(build_attributes)
build_attributes = build_attributes.merge(
pipeline: pipeline,
- project: pipeline.project,
+ project: project,
ref: pipeline.ref,
tag: pipeline.tag,
user: current_user,
trigger_request: trigger_request
)
- pipeline.builds.create(build_attributes)
+ build = pipeline.builds.create(build_attributes)
+
+ # Create the environment before the build starts. This sets its slug and
+ # makes it available as an environment variable
+ project.environments.find_or_create_by(name: build.expanded_environment_name) if
+ build.has_environment?
+
+ build
end
def new_builds
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee..240ddabec36 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
module Ci
class ImageForBuildService
def execute(project, opts)
- sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+ ref = opts[:ref]
+ sha = opts[:sha] || ref_sha(project, ref)
pipelines = project.pipelines.where(sha: sha)
- pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(pipelines.status)
+ image_name = image_for_status(pipelines.latest_status(ref))
image_path = Rails.root.join('public/ci', image_name)
+
OpenStruct.new(path: image_path, name: image_name)
end
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 4efeec0ea4e..cf28f92853e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
- = group.name
+ = group.full_name
- if group.description.present?
.description
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 71a605f33b1..7b0175af214 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,6 +1,6 @@
- page_title @group.name, "Groups"
%h3.page-title
- Group: #{@group.name}
+ Group: #{@group.full_name}
= link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index ea95e91eada..e13f404fee2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -32,7 +32,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -42,12 +42,12 @@
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
= dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
+ data: { data: todo_types_options, default_label: 'Type' } })
.filter-item.inline.actions-filter
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
+ data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 959c796ecec..b3c43286a50 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38..baa1ade5eee 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,2 @@
-.content-block.oneline-block
- = icon("sort-amount-desc")
- Most recent commits displayed first
-
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 19221e3391f..8164f61797c 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.name
+ = group.full_name
- if group_member
as
diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml
new file mode 100644
index 00000000000..f90f79337d5
--- /dev/null
+++ b/changelogs/unreleased/22864-add-environment-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Add a slug to environments
+merge_request: 7983
+author:
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
new file mode 100644
index 00000000000..66970c2a9a5
--- /dev/null
+++ b/changelogs/unreleased/24824-dropdown-items-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Add focus state to dropdown items
+merge_request:
+author:
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
new file mode 100644
index 00000000000..cc7c2604824
--- /dev/null
+++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
@@ -0,0 +1,4 @@
+---
+title: Improve bulk assignment for issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
new file mode 100644
index 00000000000..531b0f83099
--- /dev/null
+++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Improve help message for issue create slash command
+merge_request: 7850
+author:
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
new file mode 100644
index 00000000000..c6a92547c5c
--- /dev/null
+++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Added go back anchor on error pages.
+merge_request: 8087
+author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
new file mode 100644
index 00000000000..5d0adb04ef3
--- /dev/null
+++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: 25617 Fix placeholder color of todo filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
new file mode 100644
index 00000000000..cf2a622b7e6
--- /dev/null
+++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
new file mode 100644
index 00000000000..11250643a23
--- /dev/null
+++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin active tab spinach tests to rspec
+merge_request: 8037
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
new file mode 100644
index 00000000000..754af641add
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary commits order message
+merge_request: 8004
+author:
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
new file mode 100644
index 00000000000..bbd7a217493
--- /dev/null
+++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Show commit status from latest pipeline
+merge_request: 7333
+author:
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
new file mode 100644
index 00000000000..b74552e762d
--- /dev/null
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -0,0 +1,53 @@
+class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming non-unique environments'
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ # Get all [project_id, name] pairs that occur more than once
+ finder_sql = environments.
+ group(environments[:project_id], environments[:name]).
+ having(Arel.sql("COUNT(1)").gt(1)).
+ project(environments[:project_id], environments[:name]).
+ to_sql
+
+ conflicting = connection.exec_query(finder_sql)
+
+ conflicting.rows.each do |project_id, name|
+ fix_duplicates(project_id, name)
+ end
+ end
+
+ def down
+ # Nothing to do
+ end
+
+ # Rename conflicting environments by appending "-#{id}" to all but the first
+ def fix_duplicates(project_id, name)
+ environments = Arel::Table.new(:environments)
+ finder_sql = environments.
+ where(environments[:project_id].eq(project_id)).
+ where(environments[:name].eq(name)).
+ order(environments[:id].asc).
+ project(environments[:id], environments[:name]).
+ to_sql
+
+ # Now we have the data for all the conflicting rows
+ conflicts = connection.exec_query(finder_sql).rows
+ conflicts.shift # Leave the first row alone
+
+ conflicts.each do |id, name|
+ update_sql =
+ Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:name] => name + "-" + id.to_s).
+ where(environments[:id].eq(id)).
+ to_sql
+
+ connection.exec_update(update_sql, self.class.name, [])
+ end
+ end
+end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
new file mode 100644
index 00000000000..ac680c8d10f
--- /dev/null
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -0,0 +1,18 @@
+class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Making a non-unique index into a unique index'
+
+ def up
+ remove_index :environments, [:project_id, :name]
+ add_concurrent_index :environments, [:project_id, :name], unique: true
+ end
+
+ def down
+ remove_index :environments, [:project_id, :name], unique: true
+ add_concurrent_index :environments, [:project_id, :name]
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
new file mode 100644
index 00000000000..7153e6a32b1
--- /dev/null
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -0,0 +1,60 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
+
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
+ def up
+ environments = Arel::Table.new(:environments)
+
+ add_column :environments, :slug, :string
+ finder = environments.project(:id, :name)
+
+ connection.exec_query(finder.to_sql).rows.each do |id, name|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(environments).
+ set(environments[:slug] => generate_slug(name)).
+ where(environments[:id].eq(id))
+
+ connection.exec_update(updater.to_sql, self.class.name, [])
+ end
+
+ change_column_null :environments, :slug, false
+ end
+
+ def down
+ remove_column :environments, :slug
+ end
+
+ # Copy of the Environment#generate_slug implementation
+ def generate_slug(name)
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # Maximum length: 24 characters (OpenShift limitation)
+ slugified = slugified[0..23]
+
+ # Cannot end with a "-" character (Kubernetes label limitation)
+ slugified = slugified[0..-2] if slugified[-1] == "-"
+
+ # Add a random suffix, shortening the current string if necessary, if it
+ # has been slugified. This ensures uniqueness.
+ slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+ slugified
+ end
+
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
+end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
new file mode 100644
index 00000000000..e9fcef1cd45
--- /dev/null
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -0,0 +1,15 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Adding a *unique* index to environments.slug'
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :environments, [:project_id, :slug], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4711b7873af..67ff83d96d9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
end
@@ -428,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.string "external_url"
t.string "environment_type"
t.string "state", default: "available", null: false
+ t.string "slug", null: false
end
- add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+ add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
+ add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "events", force: :cascade do |t|
t.string "target_type"
@@ -737,8 +739,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.text "description_html"
t.boolean "lfs_enabled"
+ t.text "description_html"
t.integer "parent_id"
end
@@ -1219,8 +1221,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "incoming_email_token"
t.string "organization"
+ t.string "incoming_email_token"
t.boolean "authorized_projects_populated"
end
@@ -1290,4 +1292,4 @@ ActiveRecord::Schema.define(version: 20161212142807) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
-end \ No newline at end of file
+end
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 07e548aaabe..2fc5d0355b5 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -7,5 +7,6 @@ providers.
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd and Azure
-- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
+- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png
new file mode 100644
index 00000000000..12e21956715
--- /dev/null
+++ b/doc/administration/auth/img/okta_admin_panel.png
Binary files differ
diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png
new file mode 100644
index 00000000000..ee275ece369
--- /dev/null
+++ b/doc/administration/auth/img/okta_saml_settings.png
Binary files differ
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
new file mode 100644
index 00000000000..cb42b7743c5
--- /dev/null
+++ b/doc/administration/auth/okta.md
@@ -0,0 +1,160 @@
+# Okta SSO provider
+
+Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate
+with GitLab.
+
+The following documentation enables Okta as a SAML provider.
+
+## Configure the Okta application
+
+1. On Okta go to the admin section and choose to **Add an App**.
+1. When the app screen comes up you see another button to **Create an App** and
+ choose SAML 2.0 on the next screen.
+1. Now, very important, add a logo
+ (you can choose it from https://about.gitlab.com/press/). You'll have to
+ crop and resize it.
+1. Next, you'll need the to fill in the SAML general config. Here's an example
+ image.
+
+ ![Okta admin panel view](img/okta_admin_panel.png)
+
+1. The last part of the configuration is the feedback section where you can
+ just say you're a customer and creating an app for internal use.
+1. When you have your app you'll have a few tabs on the top of the app's
+ profile. Click on the SAML 2.0 config instructions button which should
+ look like the following:
+
+ ![Okta SAML settings](img/okta_saml_settings.png)
+
+1. On the screen that comes up take note of the
+ **Identity Provider Single Sign-On URL** which you'll use for the
+ `idp_sso_target_url` on your GitLab config file.
+
+1. **Before you leave Okta make sure you add your user and groups if any.**
+
+---
+
+Now that the Okta app is configured, it's time to enable it in GitLab.
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file:
+
+ **For Omnibus GitLab installations**
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ **For installations from source**
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. To allow your users to use Okta to sign up without having to manually create
+ an account first, don't forget to add the following values to your
+ configuration:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ allow_single_sign_on: ["saml"]
+ block_auto_created_users: false
+ ```
+
+1. You can also automatically link Okta users with existing GitLab users if
+ their email addresses match by adding the following setting:
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ auto_link_saml_user: true
+ ```
+
+1. Add the provider configuration.
+
+ >**Notes:**
+ >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+ of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+ installation to generate the correct value).
+ >- To get the `idp_cert_fingerprint` fingerprint, first download the
+ certificate from the Okta app you registered and then run:
+ `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
+ with the location of your certificate.
+ >- Change the value of `idp_sso_target_url`, with the value of the
+ **Identity Provider Single Sign-On URL** from the step when you
+ configured the Okta app.
+ >- Change the value of `issuer` to a unique name, which will identify the application
+ to the IdP.
+ >- Leave `name_identifier_format` as-is.
+
+ **For Omnibus GitLab installations**
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ },
+ label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
+
+
+1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
+ from source respectively for the changes to take effect.
+
+You might want to try this out on a incognito browser window.
+
+## Configuring groups
+
+>**Note:**
+Make sure the groups exist and are assigned to the Okta app.
+
+You can take a look of the [SAML documentation][saml] on external groups since
+it works the same.
+
+[okta-sso]: https://www.okta.com/products/single-sign-on/
+[saml]: ../../integration/saml.md#external-groups
+[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 06291705702..80e5d80aa41 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -44,22 +44,30 @@ as appropriate.
## Chained hooks support
-> [Introduced][93] in GitLab Shell 4.1.0.
+> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
-The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project)
-directories supporting chained execution of the hooks.
+Hooks can be also placed in `hooks/<hook_name>.d` (global) or
+`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+execution of the hooks.
+
+To look in a different directory for the global custom hooks (those in
+`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
+Omnibus installations, this can be set in `gitlab.rb`; and in source
+installations, this can be set in `gitlab-shell/config.yml`.
The hooks are searched and executed in this order:
+
1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
-1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files)
+1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
-Files in `.d` directories need to be executable and not match the backup file pattern (`*~`).
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
-The hooks of the same type are executed in order and execution stops on the first
-script exiting with non-zero value.
+The hooks of the same type are executed in order and execution stops on the
+first script exiting with a non-zero value.
## Custom error messages
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index b36cf18d459..e4f94eb7cb6 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
gitlab_rails['db_password'] = 'DB password'
postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
postgresql['listen_address'] = '0.0.0.0'
+
+ # Disable automatic database migrations
+ gitlab_rails['auto_migrate'] = false
```
1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
@@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Exit the database prompt by typing `\q` and Enter.
1. Exit the `gitlab-psql` user by running `exit` twice.
1. Run `sudo gitlab-ctl reconfigure` a final time.
-1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
---
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index f532a106bc6..b4e7bf21e35 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following:
redis['password'] = 'redis-password-goes-here'
```
-1. To prevent database migrations from running on upgrade, run:
+1. Only the primary GitLab application server should handle migrations. To
+ prevent database migrations from running on upgrade, add the following
+ configuration to your `/etc/gitlab/gitlab.rb` file:
```
- sudo touch /etc/gitlab/skip-auto-migrations
+ gitlab_rails['auto_migrate'] = false
```
- Only the primary GitLab application server should handle migrations.
-
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
### Step 2. Configuring the slave Redis instances
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 87a5fa67124..1299aca8c45 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -22,8 +22,9 @@ Example response:
[
{
"id": 1,
- "name": "Env1",
- "external_url": "https://env1.example.gitlab.com"
+ "name": "review/fix-foo",
+ "slug": "review-fix-foo-dfjre3",
+ "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
}
]
```
@@ -54,6 +55,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
@@ -85,6 +87,7 @@ Example response:
{
"id": 1,
"name": "staging",
+ "slug": "staging",
"external_url": "https://staging.example.gitlab.com"
}
```
@@ -112,6 +115,7 @@ Example response:
{
"id": 1,
"name": "deploy",
+ "slug": "deploy",
"external_url": "https://deploy.example.gitlab.com"
}
```
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 705bca6cc1f..bad0233a9ce 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -86,6 +86,13 @@ will later see, is exposed in various places within GitLab. Each time a job that
has an environment specified and succeeds, a deployment is recorded, remembering
the Git SHA and environment name.
+>**Note:**
+Starting with GitLab 8.15, the environment name is exposed to the Runner in
+two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is
+the name given in `.gitlab-ci.yml` (with any variables expanded), while the
+second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
+etc.
+
To sum up, with the above `.gitlab-ci.yml` we have achieved that:
- All branches will run the `test` and `build` jobs.
@@ -157,7 +164,7 @@ that can be found in the deployments page
job with the commit associated with it.
>**Note:**
-Bare in mind that your mileage will vary and it's entirely up to how you define
+Bear in mind that your mileage will vary and it's entirely up to how you define
the deployment process in the job's `script` whether the rollback succeeds or not.
GitLab CI is just following orders.
@@ -248,7 +255,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.example.com
+ url: https://$CI_BUILD_REF_SLUG.review.example.com
only:
- branches
except:
@@ -266,9 +273,18 @@ ones.
So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
also contain `/`, or other characters that would be invalid in a domain name or
-URL, we use `$CI_BUILD_REF_SLUG` in the `environment:url` so that the environment
-can get a specific and distinct URL for each branch. Again, the way you set up
-the webserver to serve these requests is based on your setup.
+URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
+environment can get a specific and distinct URL for each branch. In this case,
+given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up
+the web server to serve these requests is based on your setup.
+
+You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+here because it is guaranteed to be unique, but if you're using a workflow like
+[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
+environment names to be more closely based on the branch name - the example
+above would give you an URL like `https://100-do-the-thing.review.example.com`
Last but not least, we tell the job to run [`only`][only] on branches
[`except`][only] master.
@@ -300,7 +316,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
@@ -419,7 +435,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
on_stop: stop_review
only:
- branches
@@ -493,10 +509,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
## Limitations
-1. `$CI_BUILD_REF_SLUG` is not *guaranteed* to be unique, so there is a small
- chance of collisions between similarly-named branches (`fix-foo` would
- conflict with `fix/foo`, for instance). Following a well-defined workflow
- such as [GitLab Flow][gitlab-flow] can keep this from being a problem.
1. You are limited to use only the [CI predefined variables][variables] in the
`environment: name`. If you try to re-use variables defined inside `script`
as part of the environment name, it will not work.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index e0ff9756868..eb540a50606 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -52,6 +52,8 @@ version of Runner required.
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this build |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5e8d888e555..a62193700fc 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -690,7 +690,7 @@ The `stop_review_app` job is **required** to have the following keywords defined
#### dynamic environments
> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
- `$CI_BUILD_REF_SLUG` was [introduced][ce-8072] in GitLab 8.15.
+ `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15
`environment` can also represent a configuration hash with `name` and `url`.
These parameters can use any of the defined [CI variables](#variables)
@@ -703,15 +703,17 @@ deploy as review app:
stage: deploy
script: make deploy
environment:
- name: review-apps/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.review.example.com/
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
-is an [environment variable][variables] set by the Runner. If for example the
-`deploy as review app` job was run in a branch named `pow`, this environment
-should be accessible under `https://pow.review.example.com/`.
+create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. The
+`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
+for inclusion in URLs. In this case, if the `deploy as review app` job was run
+in a branch named `pow`, this environment would be accessible with an URL like
+`https://review-pow-aaaaaa.example.com/`.
This of course implies that the underlying server which hosts the application
is properly configured.
@@ -720,10 +722,6 @@ The common use case is to create dynamic environments for branches and use them
as Review Apps. You can see a simple example using Review Apps at
https://gitlab.com/gitlab-examples/review-apps-nginx/.
-`$CI_BUILD_REF_SLUG` is another environment variable set by the runner, based on
-`$CI_BUILD_REF_NAME` but lower-cased, and with some characters replaced with
-`-`, making it suitable for use in URLs and domain names.
-
### artifacts
>**Notes:**
@@ -1243,5 +1241,5 @@ CI with various languages.
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
-[ce-8072]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/xxxx
[variables]: ../variables/README.md
+[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index 76b739386a5..e81729556d8 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -5,6 +5,7 @@
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
+* [Cursors](#cursors)
---
@@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface.
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
+---
+
+## Cursors
+The mouse cursor is key in helping users understand how to interact with elements on the screen.
+
+| | |
+| :------: | :------- |
+| ![Default cursor](img/cursors-default.png) | Default cursor |
+| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons |
+| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen |
+| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. |
+| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. |
+| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. |
+
+
diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png
new file mode 100644
index 00000000000..c188ec4e351
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-default.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png
new file mode 100644
index 00000000000..86f28639982
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-ibeam.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png
new file mode 100644
index 00000000000..a9c610eaa88
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-move.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png
new file mode 100644
index 00000000000..6d247a765ac
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panclosed.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png
new file mode 100644
index 00000000000..76f2eeda831
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-panopened.png
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png
new file mode 100644
index 00000000000..d86dd955fa7
--- /dev/null
+++ b/doc/development/ux_guide/img/cursors-pointer.png
Binary files differ
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 6fcbf6f1642..1a7c13a29b4 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -65,7 +65,7 @@ the administrator console.
### Step 3. Create a new custom slash command in Mattermost
-Now that you have enabled the custom slash commands in Mattermost and opened
+Now that you have enabled custom slash commands in Mattermost and opened
the Mattermost slash commands service in GitLab, it's time to copy these values
in a new slash command.
@@ -128,20 +128,16 @@ GitLab using the Mattermost commands.
## Available slash commands
-The available slash commands so far are:
+The available slash commands are:
| Command | Description | Example |
| ------- | ----------- | ------- |
-| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` |
-| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` |
-| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-To see a list of available commands that can interact with GitLab, type the
-trigger word followed by `help`:
-
-```
-/my-project help
-```
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 5556dae2551..4eacab0c890 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.3
+sudo -u git -H git checkout v4.1.0
```
### 6. Update gitlab-workhorse
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
deleted file mode 100644
index f5bb06dea7d..00000000000
--- a/features/admin/active_tab.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-@admin
-Feature: Admin Active Tab
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Home
- Given I visit admin page
- Then the active main tab should be Overview
- And no other main tabs should be active
-
- Scenario: On Admin Projects
- Given I visit admin projects page
- Then the active main tab should be Overview
- And the active sub tab should be Projects
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Groups
- Given I visit admin groups page
- Then the active main tab should be Overview
- And the active sub tab should be Groups
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then the active main tab should be Overview
- And the active sub tab should be Users
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Logs
- Given I visit admin logs page
- Then the active main tab should be Monitoring
- And the active sub tab should be Logs
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Messages
- Given I visit admin messages page
- Then the active main tab should be Messages
- And no other main tabs should be active
-
- Scenario: On Admin Hooks
- Given I visit admin hooks page
- Then the active main tab should be Hooks
- And no other main tabs should be active
-
- Scenario: On Admin Resque
- Given I visit admin Resque page
- Then the active main tab should be Monitoring
- And the active sub tab should be Resque
- And no other main tabs should be active
- And no other sub tabs should be active
diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb
deleted file mode 100644
index 9b1689a8198..00000000000
--- a/features/steps/admin/active_tab.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Overview' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active sub tab should be Projects' do
- ensure_active_sub_tab('Projects')
- end
-
- step 'the active sub tab should be Groups' do
- ensure_active_sub_tab('Groups')
- end
-
- step 'the active sub tab should be Users' do
- ensure_active_sub_tab('Users')
- end
-
- step 'the active main tab should be Hooks' do
- ensure_active_main_tab('Hooks')
- end
-
- step 'the active main tab should be Monitoring' do
- ensure_active_main_tab('Monitoring')
- end
-
- step 'the active sub tab should be Resque' do
- ensure_active_sub_tab('Background Jobs')
- end
-
- step 'the active sub tab should be Logs' do
- ensure_active_sub_tab('Logs')
- end
-
- step 'the active main tab should be Messages' do
- ensure_active_main_tab('Messages')
- end
-end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 01c0f5072ba..dfbb3ab86dd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -629,7 +629,7 @@ module API
end
class EnvironmentBasic < Grape::Entity
- expose :id, :name, :external_url
+ expose :id, :name, :slug, :external_url
end
class Environment < EnvironmentBasic
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 80bbd9bb6e4..1a7e68f0528 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -1,6 +1,7 @@
module API
# Environments RESTfull API endpoints
class Environments < Grape::API
+ include ::API::Helpers::CustomValidators
include PaginationParams
before { authenticate! }
@@ -29,6 +30,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the environment to be created'
optional :external_url, type: String, desc: 'URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
post ':id/environments' do
authorize! :create_environment, user_project
@@ -50,6 +52,7 @@ module API
requires :environment_id, type: Integer, desc: 'The environment ID'
optional :name, type: String, desc: 'The new environment name'
optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
+ optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end
put ':id/environments/:environment_id' do
authorize! :update_environment, user_project
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
new file mode 100644
index 00000000000..0a8f3073a50
--- /dev/null
+++ b/lib/api/helpers/custom_validators.rb
@@ -0,0 +1,14 @@
+module API
+ module Helpers
+ module CustomValidators
+ class Absence < Grape::Validations::Base
+ def validate_param!(attr_name, params)
+ return if params.respond_to?(:key?) && !params.key?(attr_name)
+ raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+ end
+ end
+ end
+ end
+end
+
+Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index f77f412da56..fa234284361 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -35,11 +35,7 @@ module Gitlab
html.html_safe
end
- end
-end
-module Gitlab
- module Asciidoc
class Html5Converter < Asciidoctor::Converter::Html5Converter
extend Asciidoctor::Converter::Config
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e5406..b762d85b6e5 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
def status
@project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
+ .where(sha: @sha)
+ .latest_status(@ref) || 'unknown'
end
def metadata
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
index 1dba85c1b51..cefb6775db8 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def self.help_message
- 'issue new <title>\n<description>'
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
end
def self.allowed?(project, user)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 7c711d581e8..9e0b0e5ea98 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -131,5 +131,14 @@ module Gitlab
def kubernetes_namespace_regex_message
"can contain only letters, digits or '-', and cannot start or end with '-'"
end
+
+ def environment_slug_regex
+ @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
+ end
+
+ def environment_slug_regex_message
+ "can contain only lowercase letters, digits, and '-'. " \
+ "Must start with a letter, and cannot end with '-'"
+ end
end
end
diff --git a/public/404.html b/public/404.html
index 11b29d09a82..b3b3a0fa3f3 100644
--- a/public/404.html
+++ b/public/404.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/422.html b/public/422.html
index 9bd7cb4b7c8..119e54ad8bd 100644
--- a/public/422.html
+++ b/public/422.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/500.html b/public/500.html
index f92e8839f8d..226ef3c40ea 100644
--- a/public/500.html
+++ b/public/500.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/502.html b/public/502.html
index c2be4f130a9..f037b81bace 100644
--- a/public/502.html
+++ b/public/502.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/503.html b/public/503.html
index 8850ffce362..f946a087871 100644
--- a/public/503.html
+++ b/public/503.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
new file mode 100644
index 00000000000..16064d60ce2
--- /dev/null
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+RSpec.describe 'admin active tab' do
+ before do
+ login_as :admin
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "activates #{title} tab" do
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+ expect(page.find('.layout-nav li.active')).to have_content(title)
+ end
+ end
+
+ shared_examples 'page has active sub tab' do |title|
+ it "activates #{title} sub tab" do
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
+ expect(page.find('.sub-nav li.active')).to have_content(title)
+ end
+ end
+
+ context 'on home page' do
+ before do
+ visit admin_root_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ end
+
+ context 'on projects' do
+ before do
+ visit admin_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Projects'
+ end
+
+ context 'on groups' do
+ before do
+ visit admin_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Groups'
+ end
+
+ context 'on users' do
+ before do
+ visit admin_users_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Users'
+ end
+
+ context 'on logs' do
+ before do
+ visit admin_logs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Logs'
+ end
+
+ context 'on messages' do
+ before do
+ visit admin_broadcast_messages_path
+ end
+
+ it_behaves_like 'page has active tab', 'Messages'
+ end
+
+ context 'on hooks' do
+ before do
+ visit admin_hooks_path
+ end
+
+ it_behaves_like 'page has active tab', 'Hooks'
+ end
+
+ context 'on background jobs' do
+ before do
+ visit admin_background_jobs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Background Jobs'
+ end
+end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index bc2c087c9b9..832757b24d4 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
context 'as an allowed user', js: true do
before do
@@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
end
end
+
+ # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877
+ context 'unmarking common label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'applies label from filtered results' do
+ check 'check_all_issues'
+
+ page.within('.issues_bulk_update') do
+ click_button 'Labels'
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate')
+
+ click_link 'bug'
+ find('.dropdown-input-field', visible: true).set('wontfix')
+ click_link 'wontfix'
+ end
+
+ update_issues
+
+ page.within '.issues-holder' do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'wontfix'
+
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'wontfix'
+ end
+ end
+ end
end
context 'as a guest' do
@@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
- click_button 'Label'
+ click_button 'Labels'
wait_for_ajax
items.map do |item|
click_link item
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index cd0512a37e6..da64827b377 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -89,4 +89,12 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
+
+ it 'doesnt open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
end
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
new file mode 100644
index 00000000000..3b71c255b30
--- /dev/null
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -0,0 +1,25 @@
+/*= require extensions/object */
+
+describe('Object extensions', () => {
+ describe('assign', () => {
+ it('merges source object into target object', () => {
+ const targetObj = {};
+ const sourceObj = {
+ foo: 'bar',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('bar');
+ });
+
+ it('merges object with the same properties', () => {
+ const targetObj = {
+ foo: 'bar',
+ };
+ const sourceObj = {
+ foo: 'baz',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('baz');
+ });
+ });
+});
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 38eebb2a176..70f03021d36 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do
new_build.success!
end
- it 'reports the compound status' do
- expect(badge.status).to eq 'failed'
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index c51b10bdc69..c78cd30157e 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do
describe 'file path regex' do
it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
end
+
+ describe 'environment slug regex' do
+ def be_matched
+ match(Gitlab::Regex.environment_slug_regex)
+ end
+
+ it { expect('foo').to be_matched }
+ it { expect('foo-1').to be_matched }
+
+ it { expect('FOO').not_to be_matched }
+ it { expect('foo/1').not_to be_matched }
+ it { expect('foo.1').not_to be_matched }
+ it { expect('foo*1').not_to be_matched }
+ it { expect('9foo').not_to be_matched }
+ it { expect('foo-').not_to be_matched }
+ end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ea60c17a58a..d5f2ffcff59 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -87,6 +87,26 @@ describe Ci::Build, models: true do
end
end
+ describe '#persisted_environment' do
+ before do
+ @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'referenced literally' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it { is_expected.to eq(@environment) }
+ end
+
+ context 'referenced with a variable' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+ it { is_expected.to eq(@environment) }
+ end
+ end
+
describe '#trace' do
it { expect(build.trace).to be_nil }
@@ -328,6 +348,22 @@ describe Ci::Build, models: true do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build has an environment' do
+ before do
+ build.update(environment: 'production')
+ create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+ end
+
+ let(:environment_variables) do
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ ]
+ end
+
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
context 'when build started manually' do
before do
build.update_attributes(when: :manual)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index e78ae14b737..52dd41065e9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do
end
end
+ shared_context 'with some outdated pipelines' do
+ before do
+ create_pipeline(:canceled, 'ref', 'A')
+ create_pipeline(:success, 'ref', 'A')
+ create_pipeline(:failed, 'ref', 'B')
+ create_pipeline(:skipped, 'feature', 'C')
+ end
+
+ def create_pipeline(status, ref, sha)
+ create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ end
+ end
+
+ describe '.latest' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:pipelines) { described_class.latest.all }
+
+ it 'returns the latest pipeline for the same ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed', 'skipped')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:pipelines) { described_class.latest('ref').all }
+
+ it 'returns the latest pipeline for ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed')
+ end
+ end
+ end
+
+ describe '.latest_status' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:latest_status) { described_class.latest_status }
+
+ it 'returns the latest status for the same ref and different sha' do
+ expect(latest_status).to eq(described_class.latest.status)
+ expect(latest_status).to eq('failed')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:latest_status) { described_class.latest_status('ref') }
+
+ it 'returns the latest status for ref and different sha' do
+ expect(latest_status).to eq(described_class.latest_status('ref'))
+ expect(latest_status).to eq('failed')
+ end
+ end
+ end
+
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 0935fc0561c..74b50d2908d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -213,23 +213,19 @@ eos
end
describe '#status' do
- context 'without arguments for compound status' do
- shared_examples 'giving the status from pipeline' do
- it do
- expect(commit.status).to eq(Ci::Pipeline.status)
- end
- end
-
- context 'with pipelines' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: commit.sha)
+ context 'without ref argument' do
+ before do
+ %w[success failed created pending].each do |status|
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: status)
end
-
- it_behaves_like 'giving the status from pipeline'
end
- context 'without pipelines' do
- it_behaves_like 'giving the status from pipeline'
+ it 'gives compound status from latest pipelines' do
+ expect(commit.status).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status).to eq('pending')
end
end
@@ -255,8 +251,9 @@ eos
expect(commit.status('fix')).to eq(pipeline_from_fix.status)
end
- it 'gives compound status if ref is nil' do
- expect(commit.status(nil)).to eq(commit.status)
+ it 'gives compound status from latest pipelines if ref is nil' do
+ expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status(nil)).to eq('failed')
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c8170164898..97cbb093ed2 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:environment) { create(:environment) }
+ subject(:environment) { create(:environment) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
@@ -15,15 +15,11 @@ describe Environment, models: true do
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
- it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
-
- # To circumvent a not null violation of the name column:
- # https://github.com/thoughtbot/shoulda-matchers/issues/336
- it 'validates uniqueness of :external_url' do
- create(:environment)
+ it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:slug).is_at_most(24) }
- is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id)
- end
+ it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
+ it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
@@ -199,4 +195,38 @@ describe Environment, models: true do
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
end
end
+
+ describe '#slug' do
+ it "is automatically generated" do
+ expect(environment.slug).not_to be_nil
+ end
+
+ it "is not regenerated if name changes" do
+ original_slug = environment.slug
+ environment.update_attributes!(name: environment.name.reverse)
+
+ expect(environment.slug).to eq(original_slug)
+ end
+ end
+
+ describe '#generate_slug' do
+ SUFFIX = "-[a-z0-9]{6}"
+ {
+ "staging-12345678901234567" => "staging-123456789" + SUFFIX,
+ "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
+ "staging-1234567890123456" => "staging-1234567890123456",
+ "production" => "production",
+ "PRODUCTION" => "production" + SUFFIX,
+ "review/1-foo" => "review-1-foo" + SUFFIX,
+ "1-foo" => "env-1-foo" + SUFFIX,
+ "1/foo" => "env-1-foo" + SUFFIX,
+ "foo-" => "foo" + SUFFIX,
+ }.each do |name, matcher|
+ it "returns a slug matching #{matcher}, given #{name}" do
+ slug = described_class.new(name: name).generate_slug
+
+ expect(slug).to match(/\A#{matcher}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 893c6827a91..7d5ecfbaa64 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -272,7 +272,7 @@ describe Group, models: true do
end
describe 'nested group' do
- subject { create(:group, :nested) }
+ subject { build(:group, :nested) }
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 069c59fb5ca..9fd06bb6b23 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -128,4 +128,26 @@ describe Namespace, models: true do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
end
+
+ describe '#full_name' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_name).to eq(group.name) }
+ it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+ end
+
+ describe '#parents' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct parents' do
+ expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
+ expect(deep_nested_group.parents).to eq([group, nested_group])
+ expect(nested_group.parents).to eq([group])
+ expect(group.parents).to eq([])
+ end
+ end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 126496c43a5..b9d535bc314 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -46,6 +46,7 @@ describe API::Environments, api: true do
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('mepmep')
+ expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
end
@@ -60,6 +61,13 @@ describe API::Environments, api: true do
expect(response).to have_http_status(400)
end
+
+ it 'returns a 400 if slug is specified' do
+ post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
end
context 'a non member' do
@@ -86,6 +94,15 @@ describe API::Environments, api: true do
expect(json_response['external_url']).to eq(url)
end
+ it "won't allow slug to be changed" do
+ slug = environment.slug
+ api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
+ put api_url, slug: slug + "-foo"
+
+ expect(response).to have_http_status(400)
+ expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+ end
+
it "won't update the external_url if only the name is passed" do
url = environment.external_url
put api("/projects/#{project.id}/environments/#{environment.id}", user),
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4aadd009f3e..ceaca96e25b 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do
expect(result.manual_actions).not_to be_empty
end
end
+
+ context 'with environment' do
+ before do
+ config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates the environment' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: "review/master")).not_to be_nil
+ end
+ end
end
end