Commit 211ee4b9 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

parents 27a4aef5 ae71032d
...@@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false ...@@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API # API
gem 'grape', '~> 0.15.0' gem 'grape', '~> 0.18.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
......
...@@ -284,15 +284,15 @@ GEM ...@@ -284,15 +284,15 @@ GEM
json json
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
grape (0.15.0) grape (0.18.0)
activesupport activesupport
builder builder
hashie (>= 2.1.0) hashie (>= 2.1.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
mustermann-grape (~> 0.4.0)
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
rack-mount
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
...@@ -400,6 +400,10 @@ GEM ...@@ -400,6 +400,10 @@ GEM
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.5.5) multi_xml (0.5.5)
multipart-post (2.0.0) multipart-post (2.0.0)
mustermann (0.4.0)
tool (~> 0.2)
mustermann-grape (0.4.0)
mustermann (= 0.4.0)
mysql2 (0.3.20) mysql2 (0.3.20)
net-ldap (0.12.1) net-ldap (0.12.1)
net-ssh (3.0.1) net-ssh (3.0.1)
...@@ -505,14 +509,12 @@ GEM ...@@ -505,14 +509,12 @@ GEM
pry-rails (0.3.4) pry-rails (0.3.4)
pry (>= 0.9.10) pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.4) rack (1.6.5)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
rack rack
rack-cors (0.4.0) rack-cors (0.4.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
rack-oauth2 (1.2.3) rack-oauth2 (1.2.3)
activesupport (>= 2.3) activesupport (>= 2.3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
...@@ -743,6 +745,7 @@ GEM ...@@ -743,6 +745,7 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tool (0.2.3)
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1) nokogiri (~> 1.6.1)
...@@ -861,7 +864,7 @@ DEPENDENCIES ...@@ -861,7 +864,7 @@ DEPENDENCIES
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0) gon (~> 6.1.0)
grape (~> 0.15.0) grape (~> 0.18.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2) haml_lint (~> 0.18.2)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
content: this.editor.getValue() content: this.editor.getValue()
}, function(response) { }, function(response) {
currentPane.empty().append(response); currentPane.empty().append(response);
return currentPane.syntaxHighlight(); return currentPane.renderGFM();
}); });
} else { } else {
this.$toggleButton.show(); this.$toggleButton.show();
......
...@@ -74,7 +74,9 @@ ...@@ -74,7 +74,9 @@
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions(); new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
...@@ -144,10 +146,6 @@ ...@@ -144,10 +146,6 @@
new ZenMode(); new ZenMode();
new MergedButtons(); new MergedButtons();
break; break;
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
break;
case 'dashboard:activity': case 'dashboard:activity':
new gl.Activities(); new gl.Activities();
break; break;
......
/* 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;
};
}
...@@ -67,14 +67,15 @@ ...@@ -67,14 +67,15 @@
// The below is taken from At.js source // 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 // Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js // 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(); subtext = subtext.split(' ').pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80"); _a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF"); _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); match = regexp.exec(subtext);
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent(); $inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton = $inputContainer.find('.js-dropdown-input-clear');
this.indeterminateIds = [];
$clearButton.on('click', (function(_this) { $clearButton.on('click', (function(_this) {
// Clear click // Clear click
return function(e) { return function(e) {
...@@ -348,12 +347,12 @@ ...@@ -348,12 +347,12 @@
$el = $(this); $el = $(this);
selected = self.rowClicked($el); selected = self.rowClicked($el);
if (self.options.clicked) { 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 // Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) { if (self.options.toggleLabel) {
self.updateLabel(selected, $el, self); self.updateLabel(selected[0], $el, self);
} }
$el.trigger('blur'); $el.trigger('blur');
...@@ -444,12 +443,6 @@ ...@@ -444,12 +443,6 @@
this.resetRows(); this.resetRows();
this.addArrowKeyEvent(); this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
}
if (this.options.setActiveIds) {
this.options.setActiveIds.call(this);
}
// Makes indeterminate items effective // Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData); this.parseData(this.fullData);
...@@ -483,11 +476,6 @@ ...@@ -483,11 +476,6 @@
if (this.options.filterable) { if (this.options.filterable) {
$input.blur().val(""); $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) { if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
} }
...@@ -620,7 +608,8 @@ ...@@ -620,7 +608,8 @@
}; };
GitLabDropdown.prototype.rowClicked = function(el) { 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; fieldName = this.options.fieldName;
isInput = $(this.el).is('input'); isInput = $(this.el).is('input');
if (this.renderedData) { if (this.renderedData) {
...@@ -641,7 +630,7 @@ ...@@ -641,7 +630,7 @@
el.addClass(ACTIVE_CLASS); el.addClass(ACTIVE_CLASS);
} }
return selectedObject; return [selectedObject];
} }
field = []; field = [];
...@@ -659,6 +648,7 @@ ...@@ -659,6 +648,7 @@
} }
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS)) {
isMarking = false;
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
if (isInput) { if (isInput) {
...@@ -668,6 +658,7 @@ ...@@ -668,6 +658,7 @@
} }
} }
} else if (el.hasClass(INDETERMINATE_CLASS)) { } else if (el.hasClass(INDETERMINATE_CLASS)) {
isMarking = true;
el.addClass(ACTIVE_CLASS); el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS); el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) { if (field && field.length && value == null) {
...@@ -677,6 +668,7 @@ ...@@ -677,6 +668,7 @@
this.addInput(fieldName, value, selectedObject); this.addInput(fieldName, value, selectedObject);
} }
} else { } else {
isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) { if (!isInput) {
...@@ -697,7 +689,7 @@ ...@@ -697,7 +689,7 @@
} }
} }
return selectedObject; return [selectedObject, isMarking];
}; };
GitLabDropdown.prototype.focusTextInput = function() { GitLabDropdown.prototype.focusTextInput = function() {
......
...@@ -144,6 +144,9 @@ ...@@ -144,6 +144,9 @@
const $issuesOtherFilters = $('.issues-other-filters'); const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update'); const $issuesBulkUpdate = $('.issues_bulk_update');
this.issuableBulkActions.willUpdateLabels = false;
this.issuableBulkActions.setOriginalDropdownData();
if ($checkedIssues.length > 0) { if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) { let ids = $.map($checkedIssues, function(value) {
return $(value).data('id'); return $(value).data('id');
...@@ -155,7 +158,6 @@ ...@@ -155,7 +158,6 @@
$updateIssuesIds.val([]); $updateIssuesIds.val([]);
$issuesBulkUpdate.hide(); $issuesBulkUpdate.hide();
$issuesOtherFilters.show(); $issuesOtherFilters.show();
this.issuableBulkActions.willUpdateLabels = false;
} }
return true; return true;
}, },
......
...@@ -5,9 +5,10 @@ ...@@ -5,9 +5,10 @@
((global) => { ((global) => {
class IssuableBulkActions { class IssuableBulkActions {
constructor({ container, form, issues } = {}) { constructor({ container, form, issues, prefixId } = {}) {
this.container = container || $('.content'), this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update'); this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue'); this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this); this.form.data('bulkActions', this);
this.willUpdateLabels = false; this.willUpdateLabels = false;
...@@ -16,10 +17,6 @@ ...@@ -16,10 +17,6 @@
Issuable.initChecks(); Issuable.initChecks();
} }
getElement(selector) {
return this.container.find(selector);
}
bindEvents() { bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
} }
...@@ -73,10 +70,7 @@ ...@@ -73,10 +70,7 @@
getUnmarkedIndeterminedLabels() { getUnmarkedIndeterminedLabels() {
const result = []; const result = [];
const labelsToKeep = []; const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getElement('.labels-filter .is-indeterminate')
.each((i, el) => labelsToKeep.push($(el).data('labelId')));
this.getLabelsFromSelection().forEach((id) => { this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) { if (labelsToKeep.indexOf(id) === -1) {
...@@ -106,45 +100,65 @@ ...@@ -106,45 +100,65 @@
} }
}; };
if (this.willUpdateLabels) { if (this.willUpdateLabels) {
this.getLabelsToApply().map(function(id) { formData.update.add_label_ids = this.$labelDropdown.data('marked');
return formData.update.add_label_ids.push(id); formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
});
this.getLabelsToRemove().map(function(id) {
return formData.update.remove_label_ids.push(id);
});
} }
return formData; return formData;
} }
getLabelsToApply() { setOriginalDropdownData() {
const labelIds = []; const $labelSelect = $('.bulk-update .js-label-select');
const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); $labelSelect.data('common', this.getOriginalCommonIds());
$labels.each(function(k, label) { $labelSelect.data('marked', this.getOriginalMarkedIds());
if (label) { $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
return labelIds.push(parseInt($(label).val()));
} }
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
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);
}
/** // From issuable's initial bulk selection
* Returns Label IDs that will be removed from issue selection getOriginalIndeterminateIds() {
* @return {Array} Array of labels IDs const uniqueIds = [];
*/ const labelIds = [];
let issuableLabels = [];
getLabelsToRemove() { // Collect unique label IDs for all checked issues
const result = []; this.getElement('.selected_issue:checked').each((i, el) => {
const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
const labelsToApply = this.getLabelsToApply(); issuableLabels.forEach((labelId) => {
indeterminatedLabels.map(function(id) { // Store unique IDs
// We need to exclude label IDs that will be applied if (uniqueIds.indexOf(labelId) === -1) {
// By not doing this will cause issues from selection to not add labels at all uniqueIds.push(labelId);
if (labelsToApply.indexOf(id) === -1) {
return result.push(id);
} }
}); });
return result; // Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// 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);
} }
} }
......
...@@ -8,8 +8,9 @@ ...@@ -8,8 +8,9 @@
var _this; var _this;
_this = this; _this = this;
$('.js-label-select').each(function(i, dropdown) { $('.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); $dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text'); $toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path'); namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path'); projectPath = $dropdown.data('project-path');
...@@ -125,7 +126,7 @@ ...@@ -125,7 +126,7 @@
}); });
}); });
}; };
return $dropdown.glDropdown({ $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ return $.ajax({
...@@ -172,34 +173,41 @@ ...@@ -172,34 +173,41 @@
}); });
}, },
renderRow: function(label, instance) { 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>'); $li = $('<li>');
$a = $('<a href="#">'); $a = $('<a href="#">');
selectedClass = []; selectedClass = [];
removesAll = label.id <= 0 || (label.id == null); removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) { if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = instance.indeterminateIds; indeterminate = $dropdown.data('indeterminate') || [];
active = instance.activeIds; marked = $dropdown.data('marked') || [];
if (indeterminate.indexOf(label.id) !== -1) { if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate'); 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 // Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate'); i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) { if (i !== -1) {
selectedClass.splice(i, 1); selectedClass.splice(i, 1);
} }
selectedClass.push('is-active'); selectedClass.push('is-active');
// Add input manually
instance.addInput(this.fieldName, label.id);
} }
} } else {
if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { 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'); selectedClass.push('is-active');
} }
}
if ($dropdown.hasClass('js-multiselect') && removesAll) { if ($dropdown.hasClass('js-multiselect') && removesAll) {
selectedClass.push('dropdown-clear-active'); selectedClass.push('dropdown-clear-active');
} }
}
if (label.duplicate) { if (label.duplicate) {
spacing = 100 / label.color.length; spacing = 100 / label.color.length;
// Reduce the colors to 4 // Reduce the colors to 4
...@@ -234,7 +242,6 @@ ...@@ -234,7 +242,6 @@
// Return generated html // Return generated html
return $li.html($a).prop('outerHTML'); return $li.html($a).prop('outerHTML');
}, },
persistWhenHide: $dropdown.data('persistWhenHide'),
search: { search: {
fields: ['title'] fields: ['title']
}, },
...@@ -313,18 +320,15 @@ ...@@ -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'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) { clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page; 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) { if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent() $dropdown.parent()
...@@ -333,12 +337,11 @@ ...@@ -333,12 +337,11 @@
} }
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
_this.enableBulkLabelDropdown();
_this.setDropdownData($dropdown, isMarking, this.id(label));
return; 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 ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) { if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = []; gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
...@@ -400,17 +403,10 @@ ...@@ -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(); this.bindEvents();
} }
...@@ -423,34 +419,9 @@ ...@@ -423,34 +419,9 @@
if ($('.selected_issue:checked').length) { if ($('.selected_issue:checked').length) {
return; 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'); 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() { LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions; var issuableBulkActions;
if ($('.selected_issue:checked').length) { if ($('.selected_issue:checked').length) {
...@@ -459,8 +430,59 @@ ...@@ -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); }).call(this);
...@@ -309,7 +309,7 @@ ...@@ -309,7 +309,7 @@
} }
row = form.closest("tr"); row = form.closest("tr");
note_html = $(note.html); note_html = $(note.html);
note_html.syntaxHighlight(); note_html.renderGFM();
// is this the first note of discussion? // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) { if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
...@@ -326,7 +326,7 @@ ...@@ -326,7 +326,7 @@
discussionContainer.append(note_html); discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
$('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); $('ul.main-notes-list').append(note.discussion_html).renderGFM();
} }
} else { } else {
// append new note to all matching discussions // append new note to all matching discussions
...@@ -467,7 +467,7 @@ ...@@ -467,7 +467,7 @@
// Convert returned HTML to a jQuery object so we can modify it further // Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html); $html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $html)); gl.utils.localTimeAgo($('.js-timeago', $html));
$html.syntaxHighlight(); $html.renderGFM();
$html.find('.js-task-list-container').taskList('enable'); $html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML // Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id); $note_li = $('.note-row-' + note.id);
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
return this.renderMarkdown(mdText, (function(_this) { return this.renderMarkdown(mdText, (function(_this) {
return function(response) { return function(response) {
preview.html(response.body); preview.html(response.body);
preview.syntaxHighlight(); preview.renderGFM();
return _this.renderReferencedUsers(response.references.users, form); return _this.renderReferencedUsers(response.references.users, form);
}; };
})(this)); })(this));
......
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
//
(function() {
$.fn.renderGFM = function() {
this.find('.js-syntax-highlight').syntaxHighlight();
this.find('.js-render-math').renderMath();
};
$(document).on('ready page:load', function() {
return $('body').renderGFM();
});
}).call(this);
/* 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
//
// ### Example Markup
//
// <code class="js-render-math"></div>
//
(function() {
// Only load once
var katexLoaded = false;
// Loop over all math elements and render math
var renderWithKaTeX = function (elements) {
elements.each(function () {
var mathNode = $('<span></span>');
var $this = $(this);
var display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
// What can we do??
console.log(err.message);
}
});
};
$.fn.renderMath = function() {
var $this = this;
if ($this.length === 0) return;
if (katexLoaded) renderWithKaTeX($this);
else {
// Request CSS file so it is in the cache
$.get(gon.katex_css_url, function() {
var css = $('<link>',
{ rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
});
css.appendTo('head');
// Load KaTeX js
$.getScript(gon.katex_js_url, function() {
katexLoaded = true;
renderWithKaTeX($this); // Run KaTeX
});
});
}
};
}).call(this);
...@@ -10,8 +10,10 @@ ...@@ -10,8 +10,10 @@
// <div class="js-syntax-highlight"></div> // <div class="js-syntax-highlight"></div>
// //
(function() { (function() {
$.fn.syntaxHighlight = function() { $.fn.syntaxHighlight = function() {
var $children; var $children;
if ($(this).hasClass('js-syntax-highlight')) { if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting // Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme); return $(this).addClass(gon.user_color_scheme);
...@@ -24,8 +26,4 @@ ...@@ -24,8 +26,4 @@
} }
}; };
$(document).on('ready page:load', function() {
return $('.js-syntax-highlight').syntaxHighlight();
});
}).call(this); }).call(this);
...@@ -188,7 +188,6 @@ ...@@ -188,7 +188,6 @@
&.is-focused { &.is-focused {
background-color: $dropdown-link-hover-bg; background-color: $dropdown-link-hover-bg;
text-decoration: none; text-decoration: none;
outline: 0;
} }
&.dropdown-menu-empty-link { &.dropdown-menu-empty-link {
......
...@@ -26,6 +26,10 @@ body { ...@@ -26,6 +26,10 @@ body {
.container-limited { .container-limited {
max-width: $fixed-layout-width; max-width: $fixed-layout-width;
&.limit-container-width {
max-width: $limited-layout-width;
}
} }
......
...@@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff; ...@@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958; $error-exclamation-point: #e62958;
$border-radius-default: 2px; $border-radius-default: 2px;
$settings-icon-size: 18px; $settings-icon-size: 18px;
......
...@@ -51,8 +51,16 @@ ...@@ -51,8 +51,16 @@
.new-file-name { .new-file-name {
display: inline-block; display: inline-block;
width: 450px; max-width: 450px;
float: left; float: left;
@media(max-width: $screen-md-max) {
width: 280px;
}
@media(max-width: $screen-sm-max) {
width: 180px;
}
} }
.file-buttons { .file-buttons {
...@@ -114,3 +122,42 @@ ...@@ -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%;
}
}
}
}
}
// Limit MR description for side-by-side diff view
.limit-container-width {
.detail-page-header {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.issuable-details {
.detail-page-description,
.mr-source-target,
.mr-state-widget,
.merge-manually {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
.container-fluid {
padding-left: 0;
padding-right: 0;
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.diffs {
.mr-version-controls,
.files-changed {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
...@@ -9,7 +56,6 @@ ...@@ -9,7 +56,6 @@
.description img:not(.emoji) { .description img:not(.emoji) {
border: 1px solid $white-normal; border: 1px solid $white-normal;
padding: 5px; padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
} }
} }
......
...@@ -383,10 +383,6 @@ ul.notes { ...@@ -383,10 +383,6 @@ ul.notes {
.note-action-button { .note-action-button {
margin-left: 10px; margin-left: 10px;
} }
@media (min-width: $screen-sm-min) {
position: relative;
}
} }
.discussion-actions { .discussion-actions {
......
...@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end end
@qr_code = build_qr_code @qr_code = build_qr_code
@account_string = account_string
setup_u2f_registration setup_u2f_registration
end end
...@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private private
def build_qr_code def build_qr_code
issuer = "#{issuer_host} | #{current_user.email}" uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end end
def account_string
"#{issuer_host}:#{current_user.email}"
end
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
......
...@@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController
end end
def find_current_user_notes def find_current_user_notes
@notes = NotesFinder.new.execute(project, current_user, params) @notes = NotesFinder.new(project, current_user, params).execute.inc_author
end end
end end
...@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder ...@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
private private
def init_collection def init_collection
Issue.visible_to_user(current_user) IssuesFinder.not_restricted_by_confidentiality(current_user)
end end
def iid_pattern def iid_pattern
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
end end
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return Issue.all if user.admin?
Issue.where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
end end
class NotesFinder class NotesFinder
FETCH_OVERLAP = 5.seconds FETCH_OVERLAP = 5.seconds
def execute(project, current_user, params) # Used to filter Notes
target_type = params[:target_type] # When used with target_type and target_id this returns notes specifically for the controller
target_id = params[:target_id] #
# Default to 0 to remain compatible with old clients # Arguments:
last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) # current_user - which user check authorizations with
# project - which project to look for notes on
# params:
# target_type: string
# target_id: integer
# last_fetched_at: time
# search: string
#
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
init_collection
end
def execute
@notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
@notes
end
notes = private
case target_type
when "commit" def init_collection
project.notes.for_commit_id(target_id).non_diff_notes if @params[:target_id]
@notes = on_target(@params[:target_type], @params[:target_id])
else
@notes = notes_of_any_type
end
end
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
UnionFinder.new.find_union(note_relations, Note)
end
def noteables_for_type(noteable_type)
case noteable_type
when "issue" when "issue"
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author IssuesFinder.new(@current_user, project_id: @project.id).execute
when "merge_request" when "merge_request"
MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
else else
raise 'invalid target_type' raise 'invalid target_type'
end end
end
def notes_for_type(noteable_type)
if noteable_type == "commit"
if Ability.allowed?(@current_user, :download_code, @project)
@project.notes.where(noteable_type: 'Commit')
else
Note.none
end
else
finder = noteables_for_type(noteable_type)
@project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
end
end
def on_target(target_type, target_id)
if target_type == "commit"
notes_for_type('commit').for_commit_id(target_id)
else
target = noteables_for_type(target_type).find(target_id)
if target.respond_to?(:related_notes)
target.related_notes
else
target.notes
end
end
end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
def search(query, notes_relation = @notes)
pattern = "%#{query}%"
notes_relation.where(Note.arel_table[:note].matches(pattern))
end
# Notes changed since last fetch
# Uses overlapping intervals to avoid worrying about race conditions
def since_fetch_at(fetch_time)
# Default to 0 to remain compatible with old clients
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
# Use overlapping intervals to avoid worrying about race conditions @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end end
end end
...@@ -12,11 +12,18 @@ module GroupsHelper ...@@ -12,11 +12,18 @@ module GroupsHelper
end end
def group_title(group, name = nil, url = nil) 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 full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do content_tag :span do
full_title full_title.html_safe
end end
end end
......
...@@ -52,7 +52,7 @@ module ProjectsHelper ...@@ -52,7 +52,7 @@ module ProjectsHelper
def project_title(project) def project_title(project)
namespace_link = namespace_link =
if project.group if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group)) group_title(project.group)
else else
owner = project.namespace.owner owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
......
...@@ -9,6 +9,14 @@ module Ci ...@@ -9,6 +9,14 @@ module Ci
has_many :deployments, as: :deployable 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 :options
serialize :yaml_variables serialize :yaml_variables
...@@ -143,7 +151,7 @@ module Ci ...@@ -143,7 +151,7 @@ module Ci
end end
def expanded_environment_name def expanded_environment_name
ExpandVariables.expand(environment, variables) if environment ExpandVariables.expand(environment, simple_variables) if environment
end end
def has_environment? def has_environment?
...@@ -206,7 +214,8 @@ module Ci ...@@ -206,7 +214,8 @@ module Ci
slugified.gsub(/[^a-z0-9]/, '-')[0..62] slugified.gsub(/[^a-z0-9]/, '-')[0..62]
end end
def variables # Variables whose value does not depend on other variables
def simple_variables
variables = predefined_variables variables = predefined_variables
variables += project.predefined_variables variables += project.predefined_variables
variables += pipeline.predefined_variables variables += pipeline.predefined_variables
...@@ -219,6 +228,13 @@ module Ci ...@@ -219,6 +228,13 @@ module Ci
variables variables
end 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 def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff) merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id) .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
......
...@@ -88,8 +88,24 @@ module Ci ...@@ -88,8 +88,24 @@ module Ci
end end
# ref can't be HEAD or SHA, can only be branch/tag name # 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) def self.latest_successful_for(ref)
where(ref: ref).order(id: :desc).success.first success.latest(ref).first
end end
def self.truncate_sha(sha) def self.truncate_sha(sha)
......
...@@ -228,13 +228,9 @@ class Commit ...@@ -228,13 +228,9 @@ class Commit
def status(ref = nil) def status(ref = nil)
@statuses ||= {} @statuses ||= {}
if @statuses.key?(ref) return @statuses[ref] if @statuses.key?(ref)
@statuses[ref]
elsif ref @statuses[ref] = pipelines.latest_status(ref)
@statuses[ref] = pipelines.where(ref: ref).status
else
@statuses[ref] = pipelines.status
end
end end
def revert_branch_name def revert_branch_name
......
...@@ -30,7 +30,7 @@ module Milestoneish ...@@ -30,7 +30,7 @@ module Milestoneish
end end
def issues_visible_to_user(user) def issues_visible_to_user(user)
issues.visible_to_user(user) IssuesFinder.new(user).execute.where(id: issues)
end end
def upcoming? def upcoming?
......
class Environment < ActiveRecord::Base 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 belongs_to :project, required: true, validate: true
has_many :deployments has_many :deployments
before_validation :nullify_external_url before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
before_save :set_environment_type before_save :set_environment_type
validates :name, validates :name,
...@@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base ...@@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base
format: { with: Gitlab::Regex.environment_name_regex, format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message } 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, validates :external_url,
uniqueness: { scope: :project_id }, uniqueness: { scope: :project_id },
length: { maximum: 255 }, length: { maximum: 255 },
...@@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base ...@@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base
state :stopped state :stopped
end 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) def recently_updated_on_branch?(ref)
ref.to_s == last_deployment.try(:ref) ref.to_s == last_deployment.try(:ref)
end end
...@@ -107,4 +127,41 @@ class Environment < ActiveRecord::Base ...@@ -107,4 +127,41 @@ class Environment < ActiveRecord::Base
action.expanded_environment_name == environment action.expanded_environment_name == environment
end end
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 end
...@@ -83,7 +83,7 @@ class Group < Namespace ...@@ -83,7 +83,7 @@ class Group < Namespace
end end
def human_name def human_name
name full_name
end end
def visibility_level_field def visibility_level_field
......
...@@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base ...@@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base
attributes attributes
end end
class << self
private
# Returns the project that the current scope belongs to if any, nil otherwise.
#
# Examples:
# - my_project.issues.without_due_date.owner_project => my_project
# - Issue.all.owner_project => nil
def owner_project
# No owner if we're not being called from an association
return unless all.respond_to?(:proxy_association)
owner = all.proxy_association.owner
# Check if the association is or belongs to a project
if owner.is_a?(Project)
owner
else
begin
owner.association(:project).target
rescue ActiveRecord::AssociationNotFoundError
nil
end
end
end
end
def self.visible_to_user(user)
return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return all if user.admin?
# Check if we are scoped to a specific project's issues
if owner_project
if owner_project.team.member?(user, Gitlab::Access::REPORTER)
# If the project is authorized for the user, they can see all issues in the project
return all
else
# else only non confidential and authored/assigned to them
return where('issues.confidential IS NULL OR issues.confidential IS FALSE
OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
user_id: user.id)
end
end
where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
......
...@@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch? should_remove_source_branch? || force_remove_source_branch?
end end
def mr_and_commit_notes def related_notes
# Fetch comments only from last 100 commits # Fetch comments only from last 100 commits
commits_for_notes_limit = 100 commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id) commit_ids = commits.last(commits_for_notes_limit).map(&:id)
...@@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base
end end
def discussions def discussions
@discussions ||= self.mr_and_commit_notes. @discussions ||= self.related_notes.
inc_relations_for_view. inc_relations_for_view.
fresh. fresh.
discussions discussions
......
...@@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base ...@@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base
end end
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 private
def repository_storage_paths def repository_storage_paths
......
...@@ -107,23 +107,6 @@ class Note < ActiveRecord::Base ...@@ -107,23 +107,6 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes). Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h map { |d| [d.line_code, d] }.to_h
end end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
# as_user - Limit results to those viewable by a specific user
#
# Returns an ActiveRecord::Relation.
def search(query, as_user: nil)
table = arel_table
pattern = "%#{query}%"
Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
where(table[:note].matches(pattern)).
merge(Issue.visible_to_user(as_user))
end
end end
def cross_reference? def cross_reference?
......
...@@ -95,7 +95,8 @@ class Project < ActiveRecord::Base ...@@ -95,7 +95,8 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :slack_service, dependent: :destroy has_one :mattermost_notification_service, dependent: :destroy
has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy
......
require 'slack-notifier' require 'slack-notifier'
class SlackService module ChatMessage
class BaseMessage class BaseMessage
def initialize(params) def initialize(params)
raise NotImplementedError raise NotImplementedError
......
class SlackService module ChatMessage
class BuildMessage < BaseMessage class BuildMessage < BaseMessage
attr_reader :sha attr_reader :sha
attr_reader :ref_type attr_reader :ref_type
......
class SlackService module ChatMessage
class IssueMessage < BaseMessage class IssueMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :title attr_reader :title
......
class SlackService module ChatMessage
class MergeMessage < BaseMessage class MergeMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :project_name attr_reader :project_name
......
class SlackService module ChatMessage
class NoteMessage < BaseMessage class NoteMessage < BaseMessage
attr_reader :message attr_reader :message
attr_reader :user_name attr_reader :user_name
......
class SlackService module ChatMessage
class PipelineMessage < BaseMessage class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url, attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id :user_name, :duration, :pipeline_id
......
class SlackService module ChatMessage
class PushMessage < BaseMessage class PushMessage < BaseMessage
attr_reader :after attr_reader :after
attr_reader :before attr_reader :before
......
class SlackService module ChatMessage
class WikiPageMessage < BaseMessage class WikiPageMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :title attr_reader :title
......
class SlackService < Service # Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
class ChatNotificationService < Service
include ChatMessage
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated? validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties def initialize_properties
...@@ -14,35 +21,8 @@ class SlackService < Service ...@@ -14,35 +21,8 @@ class SlackService < Service
end end
end end
def title def can_test?
'Slack' valid?
end
def description
'A team communication tool for the 21st century'
end
def to_param
'slack'
end
def help
'This service sends notifications to your Slack channel.<br/>
To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
and enter the Webhook URL below.'
end
def fields
default_fields =
[
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
default_fields + build_event_channels
end end
def supported_events def supported_events
...@@ -67,21 +47,16 @@ class SlackService < Service ...@@ -67,21 +47,16 @@ class SlackService < Service
message = get_message(object_kind, data) message = get_message(object_kind, data)
if message return false unless message
opt = {}
event_channel = get_channel_field(object_kind) || channel opt = {}
opt[:channel] = event_channel if event_channel opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
opt[:username] = username if username opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt) notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true true
else
false
end
end end
def event_channel_names def event_channel_names
...@@ -96,6 +71,10 @@ class SlackService < Service ...@@ -96,6 +71,10 @@ class SlackService < Service
fields.reject { |field| field[:name].end_with?('channel') } fields.reject { |field| field[:name].end_with?('channel') }
end end
def default_channel
raise NotImplementedError
end
private private
def get_message(object_kind, data) def get_message(object_kind, data)
...@@ -124,7 +103,7 @@ class SlackService < Service ...@@ -124,7 +103,7 @@ class SlackService < Service
def build_event_channels def build_event_channels
supported_events.reduce([]) do |channels, event| supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
end end
end end
...@@ -166,11 +145,3 @@ class SlackService < Service ...@@ -166,11 +145,3 @@ class SlackService < Service
end end
end end
end end
require "slack_service/issue_message"
require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
require "slack_service/pipeline_message"
require "slack_service/wiki_page_message"
# Base class for Chat services # Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from. # This class is not meant to be used directly, but only to inherit from.
class ChatService < Service class ChatService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
......
class MattermostNotificationService < ChatNotificationService
def title
'Mattermost notifications'
end
def description
'Receive event notifications in Mattermost'
end
def to_param
'mattermost_notification'
end
def help
'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service:
<ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel
"#town-square"
end
end
class SlackNotificationService < ChatNotificationService
def title
'Slack notifications'
end
def description
'Receive event notifications in Slack'
end
def to_param
'slack_notification'
end
def help
'This service sends notifications about projects events to Slack channels.<br />
To setup this service:
<ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel
"#general"
end
end
...@@ -220,7 +220,8 @@ class Service < ActiveRecord::Base ...@@ -220,7 +220,8 @@ class Service < ActiveRecord::Base
pivotaltracker pivotaltracker
pushover pushover
redmine redmine
slack mattermost_notification
slack_notification
teamcity teamcity
] ]
end end
......
...@@ -10,18 +10,29 @@ module Ci ...@@ -10,18 +10,29 @@ module Ci
end end
end end
def project
pipeline.project
end
private private
def create_build(build_attributes) def create_build(build_attributes)
build_attributes = build_attributes.merge( build_attributes = build_attributes.merge(
pipeline: pipeline, pipeline: pipeline,
project: pipeline.project, project: project,
ref: pipeline.ref, ref: pipeline.ref,
tag: pipeline.tag, tag: pipeline.tag,
user: current_user, user: current_user,
trigger_request: trigger_request 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 end
def new_builds def new_builds
......
module Ci module Ci
class ImageForBuildService class ImageForBuildService
def execute(project, opts) 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 = 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) image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(path: image_path, name: image_name) OpenStruct.new(path: image_path, name: image_name)
end end
......
class ArtifactUploader < CarrierWave::Uploader::Base class ArtifactUploader < GitlabUploader
storage :file storage :file
attr_accessor :build, :field attr_accessor :build, :field
...@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base ...@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
def exists? def exists?
file.try(:exists?) file.try(:exists?)
end end
def move_to_cache
true
end
def move_to_store
true
end
end end
class AttachmentUploader < CarrierWave::Uploader::Base class AttachmentUploader < GitlabUploader
include UploaderHelper include UploaderHelper
storage :file storage :file
......
class AvatarUploader < CarrierWave::Uploader::Base class AvatarUploader < GitlabUploader
include UploaderHelper include UploaderHelper
storage :file storage :file
......
class FileUploader < CarrierWave::Uploader::Base class FileUploader < GitlabUploader
include UploaderHelper include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
......
class GitlabUploader < CarrierWave::Uploader::Base
# Reduce disk IO
def move_to_cache
true
end
# Reduce disk IO
def move_to_store
true
end
end
class LfsObjectUploader < CarrierWave::Uploader::Base class LfsObjectUploader < GitlabUploader
storage :file storage :file
def store_dir def store_dir
...@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base ...@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
"#{Gitlab.config.lfs.storage_path}/tmp/cache" "#{Gitlab.config.lfs.storage_path}/tmp/cache"
end end
def move_to_cache
true
end
def move_to_store
true
end
def exists? def exists?
file.try(:exists?) file.try(:exists?)
end end
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to [:admin, group], class: 'group-name' do = link_to [:admin, group], class: 'group-name' do
= group.name = group.full_name
- if group.description.present? - if group.description.present?
.description .description
......
- page_title @group.name, "Groups" - page_title @group.name, "Groups"
%h3.page-title %h3.page-title
Group: #{@group.name} Group: #{@group.full_name}
= link_to admin_group_edit_path(@group), class: "btn pull-right" do = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
- if params[:project_id].present? - if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id]) = 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', = 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 .filter-item.inline
- if params[:author_id].present? - if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id]) = hidden_field_tag(:author_id, params[:author_id])
...@@ -42,12 +42,12 @@ ...@@ -42,12 +42,12 @@
- if params[:type].present? - if params[:type].present?
= hidden_field_tag(:type, params[:type]) = 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', = 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 .filter-item.inline.actions-filter
- if params[:action_id].present? - if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id]) = 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', = 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 .pull-right
.dropdown.inline.prepend-left-10 .dropdown.inline.prepend-left-10
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone. To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0 %p.prepend-top-0.append-bottom-0
Account: Account:
= current_user.email = @account_string
%p.prepend-top-0.append-bottom-0 %p.prepend-top-0.append-bottom-0
Key: Key:
= current_user.otp_secret.scan(/.{4}/).join(' ') = current_user.otp_secret.scan(/.{4}/).join(' ')
......
- @content_class = "limit-container-width"
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description - page_description @issue.description
- page_card_attributes @issue.card_attributes - page_card_attributes @issue.card_attributes
......
%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 - if @bulk_edit
.issue-check .issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = merge_request.mr_and_commit_notes.user.count - note_count = merge_request.related_notes.user.count
%li %li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
- @content_class = "limit-container-width"
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description - page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes - page_card_attributes @merge_request.card_attributes
...@@ -41,7 +42,7 @@ ...@@ -41,7 +42,7 @@
= render "projects/merge_requests/widget/show.html.haml" = render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default.append-bottom-default .merge-manually.light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the You can also accept this merge request manually using the
= succeed '.' do = succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
...@@ -53,7 +54,7 @@ ...@@ -53,7 +54,7 @@
%li.notes-tab %li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count %span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project - if @merge_request.source_project
%li.commits-tab %li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
......
.content-block.oneline-block
= icon("sort-amount-desc")
Most recent commits displayed first
%ol#commits-list.list-unstyled %ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to group, class: 'group-name' do = link_to group, class: 'group-name' do
= group.name = group.full_name
- if group_member - if group_member
as as
......
---
title: Add a slug to environments
merge_request: 7983
author:
---
title: Add focus state to dropdown items
merge_request:
author:
---
title: Improve bulk assignment for issuables
merge_request:
author:
---
title: Improve help message for issue create slash command
merge_request: 7850
author:
---
title: Added go back anchor on error pages.
merge_request: 8087
author:
---
title: 25617 Fix placeholder color of todo filters
merge_request:
author:
---
title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
merge_request: 8003
author: Munken
---
title: Replace static fixture for abuse_reports_spec
merge_request: 7644
author: winniehell
---
title: Add GitLab host to 2FA QR code and manual info
merge_request: 6941
author:
---
title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
merge_request:
author: twonegatives
---
title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
merge_request:
author:
---
title: 'Gem update: Update grape to 0.18.0'
merge_request:
author: Robert Schilling
---
title: Replace Rack::Multipart with GitLab-Workhorse based solution
merge_request: 5867
author:
---
title: Create mattermost service
merge_request:
author:
---
title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
merge_request:
author:
---
title: Fix missing Note access checks by moving Note#search to updated NoteFinder
merge_request:
author:
---
title: Move admin active tab spinach tests to rspec
merge_request: 8037
author: Semyon Pupkov
---
title: Remove unnecessary commits order message
merge_request: 8004
author:
---
title: Show commit status from latest pipeline
merge_request: 7333
author:
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
# #
# Parameters filtered: # Parameters filtered:
# - Password (:password, :password_confirmation) # - Password (:password, :password_confirmation)
# - Private tokens (:private_token, :authentication_token) # - Private tokens
# - Two-factor tokens (:otp_attempt) # - Two-factor tokens (:otp_attempt)
# - Repo/Project Import URLs (:import_url) # - Repo/Project Import URLs (:import_url)
# - Build variables (:variables) # - Build variables (:variables)
...@@ -60,11 +60,13 @@ module Gitlab ...@@ -60,11 +60,13 @@ module Gitlab
encrypted_key encrypted_key
hook hook
import_url import_url
incoming_email_token
key key
otp_attempt otp_attempt
password password
password_confirmation password_confirmation
private_token private_token
runners_token
secret_token secret_token
sentry_dsn sentry_dsn
variables variables
...@@ -85,6 +87,8 @@ module Gitlab ...@@ -85,6 +87,8 @@ module Gitlab
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
config.assets.precompile << "notify.css" config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css" config.assets.precompile << "mailers/*.css"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "network/network_bundle.js"
......
# Touch the lexers so it is registered with Rouge
Rouge::Lexers::Math
Rails.application.configure do |config|
config.middleware.use(Gitlab::Middleware::Multipart)
end
...@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines ...@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
def job_attributes(pipeline, opts) def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now created_at: Time.now, updated_at: Time.now
}.merge(opts) }.merge(opts)
end end
......
# rubocop:disable all # rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change def change
SlackService.all.each do |slack_service| SlackNotificationService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token'] token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain'] subdomain = slack_service.properties['subdomain']
......
...@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration ...@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
end end
def down def down
Route.delete_all(source_type: 'Namespace') execute("DELETE FROM routes WHERE source_type = 'Namespace'")
end end
end end
...@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration ...@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
DOWNTIME_REASON = 'No new projects should be created during data copy' DOWNTIME_REASON = 'No new projects should be created during data copy'
def up def up
if Gitlab::Database.postgresql?
execute <<-EOF execute <<-EOF
INSERT INTO routes INSERT INTO routes (source_id, source_type, path)
(source_id, source_type, path) (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
(SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
INNER JOIN namespaces ON projects.namespace_id = namespaces.id) ORDER BY namespaces.path, projects.path, projects.id DESC)
EOF EOF
else
execute <<-EOF
INSERT INTO routes (source_id, source_type, path)
(SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path)
FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
EOF
end
end end
def down def down
Route.delete_all(source_type: 'Project') execute("DELETE FROM routes WHERE source_type = 'Project'")
end end
end end
...@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration ...@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| # We can skip this migration when running a PostgreSQL database because
path = connection.quote(row['path']) # we use an optimized query in the "FillProjectsRoutesTable" migration
execute(%Q{ # to fill these values that avoid duplicate entries in the routes table.
DELETE FROM #{quote_table_name(:routes)} return unless Gitlab::Database.mysql?
WHERE path = #{path}
AND id != ( execute <<-EOF
SELECT id FROM ( DELETE duplicated_rows.*
SELECT max(id) AS id FROM routes AS duplicated_rows
FROM #{quote_table_name(:routes)} INNER JOIN (
WHERE path = #{path} SELECT path, MAX(id) as max_id
) max_ids FROM routes
) GROUP BY path
}) HAVING COUNT(*) > 1
end ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
EOF
end end
def down def down
......
...@@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration ...@@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration
end end
def down def down
if index_exists?(:namespaces, :name) if index_exists?(:namespaces, [:name, :parent_id])
remove_index :namespaces, [:name, :parent_id] remove_index :namespaces, [:name, :parent_id]
end end
end end
......
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
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment