Merge branch 'master' into auto-pipelines-vue
This commit is contained in:
commit
211ee4b97c
|
@ -1 +1 @@
|
|||
4.0.3
|
||||
4.1.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.1.1
|
||||
1.2.0
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
|
|||
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
|
||||
|
||||
# API
|
||||
gem 'grape', '~> 0.15.0'
|
||||
gem 'grape', '~> 0.18.0'
|
||||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
||||
|
||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -284,15 +284,15 @@ GEM
|
|||
json
|
||||
multi_json
|
||||
request_store (>= 1.0)
|
||||
grape (0.15.0)
|
||||
grape (0.18.0)
|
||||
activesupport
|
||||
builder
|
||||
hashie (>= 2.1.0)
|
||||
multi_json (>= 1.3.2)
|
||||
multi_xml (>= 0.5.2)
|
||||
mustermann-grape (~> 0.4.0)
|
||||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
rack-mount
|
||||
virtus (>= 1.0.0)
|
||||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
|
@ -400,6 +400,10 @@ GEM
|
|||
multi_json (1.12.1)
|
||||
multi_xml (0.5.5)
|
||||
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)
|
||||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
|
@ -505,14 +509,12 @@ GEM
|
|||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
rack (1.6.4)
|
||||
rack (1.6.5)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (4.4.1)
|
||||
rack
|
||||
rack-cors (0.4.0)
|
||||
rack-mount (0.8.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-oauth2 (1.2.3)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
@ -743,6 +745,7 @@ GEM
|
|||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
tool (0.2.3)
|
||||
truncato (0.7.8)
|
||||
htmlentities (~> 4.3.1)
|
||||
nokogiri (~> 1.6.1)
|
||||
|
@ -861,7 +864,7 @@ DEPENDENCIES
|
|||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.2)
|
||||
gon (~> 6.1.0)
|
||||
grape (~> 0.15.0)
|
||||
grape (~> 0.18.0)
|
||||
grape-entity (~> 0.6.0)
|
||||
haml_lint (~> 0.18.2)
|
||||
hamlit (~> 2.6.1)
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
content: this.editor.getValue()
|
||||
}, function(response) {
|
||||
currentPane.empty().append(response);
|
||||
return currentPane.syntaxHighlight();
|
||||
return currentPane.renderGFM();
|
||||
});
|
||||
} else {
|
||||
this.$toggleButton.show();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
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()));
|
||||
}
|
||||
});
|
||||
return labelIds;
|
||||
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 = [];
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return result;
|
||||
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
|
||||
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);
|
||||
});
|
||||
// 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 @@
|
|||
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);
|
||||
|
|
|
@ -309,7 +309,7 @@
|
|||
}
|
||||
row = form.closest("tr");
|
||||
note_html = $(note.html);
|
||||
note_html.syntaxHighlight();
|
||||
note_html.renderGFM();
|
||||
// is this the first note of discussion?
|
||||
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
|
||||
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
|
||||
|
@ -326,7 +326,7 @@
|
|||
discussionContainer.append(note_html);
|
||||
// Init discussion on 'Discussion' page if it is merge request page
|
||||
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 {
|
||||
// append new note to all matching discussions
|
||||
|
@ -467,7 +467,7 @@
|
|||
// Convert returned HTML to a jQuery object so we can modify it further
|
||||
$html = $(note.html);
|
||||
gl.utils.localTimeAgo($('.js-timeago', $html));
|
||||
$html.syntaxHighlight();
|
||||
$html.renderGFM();
|
||||
$html.find('.js-task-list-container').taskList('enable');
|
||||
// Find the note's `li` element by ID and replace it with the updated HTML
|
||||
$note_li = $('.note-row-' + note.id);
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
return this.renderMarkdown(mdText, (function(_this) {
|
||||
return function(response) {
|
||||
preview.html(response.body);
|
||||
preview.syntaxHighlight();
|
||||
preview.renderGFM();
|
||||
return _this.renderReferencedUsers(response.references.users, form);
|
||||
};
|
||||
})(this));
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/* 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);
|
|
@ -0,0 +1,55 @@
|
|||
/* 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 @@
|
|||
// <div class="js-syntax-highlight"></div>
|
||||
//
|
||||
(function() {
|
||||
|
||||
$.fn.syntaxHighlight = function() {
|
||||
var $children;
|
||||
|
||||
if ($(this).hasClass('js-syntax-highlight')) {
|
||||
// Given the element itself, apply highlighting
|
||||
return $(this).addClass(gon.user_color_scheme);
|
||||
|
@ -24,8 +26,4 @@
|
|||
}
|
||||
};
|
||||
|
||||
$(document).on('ready page:load', function() {
|
||||
return $('.js-syntax-highlight').syntaxHighlight();
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
|
|
@ -188,7 +188,6 @@
|
|||
&.is-focused {
|
||||
background-color: $dropdown-link-hover-bg;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.dropdown-menu-empty-link {
|
||||
|
|
|
@ -26,6 +26,10 @@ body {
|
|||
|
||||
.container-limited {
|
||||
max-width: $fixed-layout-width;
|
||||
|
||||
&.limit-container-width {
|
||||
max-width: $limited-layout-width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff;
|
|||
$progress-color: #c0392b;
|
||||
$header-height: 50px;
|
||||
$fixed-layout-width: 1280px;
|
||||
$limited-layout-width: 990px;
|
||||
$gl-avatar-size: 40px;
|
||||
$error-exclamation-point: #e62958;
|
||||
$border-radius-default: 2px;
|
||||
$settings-icon-size: 18px;
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,50 @@
|
|||
// 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 {
|
||||
section {
|
||||
.issuable-discussion {
|
||||
|
@ -9,7 +56,6 @@
|
|||
.description img:not(.emoji) {
|
||||
border: 1px solid $white-normal;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,10 +383,6 @@ ul.notes {
|
|||
.note-action-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-actions {
|
||||
|
|
|
@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
end
|
||||
|
||||
@qr_code = build_qr_code
|
||||
@account_string = account_string
|
||||
setup_u2f_registration
|
||||
end
|
||||
|
||||
|
@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
private
|
||||
|
||||
def build_qr_code
|
||||
issuer = "#{issuer_host} | #{current_user.email}"
|
||||
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
|
||||
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
|
||||
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
|
||||
end
|
||||
|
||||
def account_string
|
||||
"#{issuer_host}:#{current_user.email}"
|
||||
end
|
||||
|
||||
def issuer_host
|
||||
Gitlab.config.gitlab.host
|
||||
end
|
||||
|
|
|
@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
|
|||
private
|
||||
|
||||
def init_collection
|
||||
Issue.visible_to_user(current_user)
|
||||
IssuesFinder.not_restricted_by_confidentiality(current_user)
|
||||
end
|
||||
|
||||
def iid_pattern
|
||||
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
|
||||
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
|
||||
|
|
|
@ -1,27 +1,102 @@
|
|||
class NotesFinder
|
||||
FETCH_OVERLAP = 5.seconds
|
||||
|
||||
def execute(project, current_user, params)
|
||||
target_type = params[:target_type]
|
||||
target_id = params[:target_id]
|
||||
# Default to 0 to remain compatible with old clients
|
||||
last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
|
||||
# Used to filter Notes
|
||||
# When used with target_type and target_id this returns notes specifically for the controller
|
||||
#
|
||||
# Arguments:
|
||||
# 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
|
||||
|
||||
notes =
|
||||
case target_type
|
||||
when "commit"
|
||||
project.notes.for_commit_id(target_id).non_diff_notes
|
||||
when "issue"
|
||||
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
|
||||
when "merge_request"
|
||||
MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author
|
||||
when "snippet", "project_snippet"
|
||||
project.snippets.find(target_id).notes
|
||||
def execute
|
||||
@notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
|
||||
@notes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_collection
|
||||
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"
|
||||
IssuesFinder.new(@current_user, project_id: @project.id).execute
|
||||
when "merge_request"
|
||||
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
|
||||
when "snippet", "project_snippet"
|
||||
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
|
||||
else
|
||||
raise 'invalid target_type'
|
||||
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
|
||||
raise 'invalid target_type'
|
||||
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
|
||||
|
||||
# Use overlapping intervals to avoid worrying about race conditions
|
||||
notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
|
||||
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)
|
||||
|
||||
@notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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 += ' · '.html_safe + link_to(simple_sanitize(name), url) if name
|
||||
|
||||
content_tag :span do
|
||||
full_title
|
||||
full_title.html_safe
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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('-')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ module Milestoneish
|
|||
end
|
||||
|
||||
def issues_visible_to_user(user)
|
||||
issues.visible_to_user(user)
|
||||
IssuesFinder.new(user).execute.where(id: issues)
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -83,7 +83,7 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
def human_name
|
||||
name
|
||||
full_name
|
||||
end
|
||||
|
||||
def visibility_level_field
|
||||
|
|
|
@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base
|
|||
attributes
|
||||
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
|
||||
'#'
|
||||
end
|
||||
|
|
|
@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
should_remove_source_branch? || force_remove_source_branch?
|
||||
end
|
||||
|
||||
def mr_and_commit_notes
|
||||
def related_notes
|
||||
# Fetch comments only from last 100 commits
|
||||
commits_for_notes_limit = 100
|
||||
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
|
||||
|
@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def discussions
|
||||
@discussions ||= self.mr_and_commit_notes.
|
||||
@discussions ||= self.related_notes.
|
||||
inc_relations_for_view.
|
||||
fresh.
|
||||
discussions
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -107,23 +107,6 @@ class Note < ActiveRecord::Base
|
|||
Discussion.for_diff_notes(active_notes).
|
||||
map { |d| [d.line_code, d] }.to_h
|
||||
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
|
||||
|
||||
def cross_reference?
|
||||
|
|
|
@ -95,7 +95,8 @@ class Project < ActiveRecord::Base
|
|||
has_one :asana_service, dependent: :destroy
|
||||
has_one :gemnasium_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 :bamboo_service, dependent: :destroy
|
||||
has_one :teamcity_service, dependent: :destroy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'slack-notifier'
|
||||
|
||||
class SlackService
|
||||
module ChatMessage
|
||||
class BaseMessage
|
||||
def initialize(params)
|
||||
raise NotImplementedError
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class BuildMessage < BaseMessage
|
||||
attr_reader :sha
|
||||
attr_reader :ref_type
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class IssueMessage < BaseMessage
|
||||
attr_reader :user_name
|
||||
attr_reader :title
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class MergeMessage < BaseMessage
|
||||
attr_reader :user_name
|
||||
attr_reader :project_name
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class NoteMessage < BaseMessage
|
||||
attr_reader :message
|
||||
attr_reader :user_name
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class PipelineMessage < BaseMessage
|
||||
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
|
||||
:user_name, :duration, :pipeline_id
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class PushMessage < BaseMessage
|
||||
attr_reader :after
|
||||
attr_reader :before
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class WikiPageMessage < BaseMessage
|
||||
attr_reader :user_name
|
||||
attr_reader :title
|
|
@ -1,6 +1,13 @@
|
|||
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
|
||||
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
|
||||
|
||||
validates :webhook, presence: true, url: true, if: :activated?
|
||||
|
||||
def initialize_properties
|
||||
|
@ -14,35 +21,8 @@ class SlackService < Service
|
|||
end
|
||||
end
|
||||
|
||||
def title
|
||||
'Slack'
|
||||
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
|
||||
def can_test?
|
||||
valid?
|
||||
end
|
||||
|
||||
def supported_events
|
||||
|
@ -67,21 +47,16 @@ class SlackService < Service
|
|||
|
||||
message = get_message(object_kind, data)
|
||||
|
||||
if message
|
||||
opt = {}
|
||||
return false unless message
|
||||
|
||||
event_channel = get_channel_field(object_kind) || channel
|
||||
opt = {}
|
||||
|
||||
opt[:channel] = event_channel if event_channel
|
||||
opt[:username] = username if username
|
||||
opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
|
||||
opt[:username] = username if username
|
||||
notifier = Slack::Notifier.new(webhook, opt)
|
||||
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
|
||||
|
||||
notifier = Slack::Notifier.new(webhook, opt)
|
||||
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def event_channel_names
|
||||
|
@ -96,6 +71,10 @@ class SlackService < Service
|
|||
fields.reject { |field| field[:name].end_with?('channel') }
|
||||
end
|
||||
|
||||
def default_channel
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_message(object_kind, data)
|
||||
|
@ -124,7 +103,7 @@ class SlackService < Service
|
|||
|
||||
def build_event_channels
|
||||
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
|
||||
|
||||
|
@ -166,11 +145,3 @@ class SlackService < Service
|
|||
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"
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
default_value_for :category, 'chat'
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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
|
|
@ -0,0 +1,40 @@
|
|||
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
|
|||
pivotaltracker
|
||||
pushover
|
||||
redmine
|
||||
slack
|
||||
mattermost_notification
|
||||
slack_notification
|
||||
teamcity
|
||||
]
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class ArtifactUploader < CarrierWave::Uploader::Base
|
||||
class ArtifactUploader < GitlabUploader
|
||||
storage :file
|
||||
|
||||
attr_accessor :build, :field
|
||||
|
@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
|
|||
def exists?
|
||||
file.try(:exists?)
|
||||
end
|
||||
|
||||
def move_to_cache
|
||||
true
|
||||
end
|
||||
|
||||
def move_to_store
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class AttachmentUploader < CarrierWave::Uploader::Base
|
||||
class AttachmentUploader < GitlabUploader
|
||||
include UploaderHelper
|
||||
|
||||
storage :file
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class AvatarUploader < CarrierWave::Uploader::Base
|
||||
class AvatarUploader < GitlabUploader
|
||||
include UploaderHelper
|
||||
|
||||
storage :file
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class FileUploader < CarrierWave::Uploader::Base
|
||||
class FileUploader < GitlabUploader
|
||||
include UploaderHelper
|
||||
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class GitlabUploader < CarrierWave::Uploader::Base
|
||||
# Reduce disk IO
|
||||
def move_to_cache
|
||||
true
|
||||
end
|
||||
|
||||
# Reduce disk IO
|
||||
def move_to_store
|
||||
true
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
class LfsObjectUploader < CarrierWave::Uploader::Base
|
||||
class LfsObjectUploader < GitlabUploader
|
||||
storage :file
|
||||
|
||||
def store_dir
|
||||
|
@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
|
|||
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
|
||||
end
|
||||
|
||||
def move_to_cache
|
||||
true
|
||||
end
|
||||
|
||||
def move_to_store
|
||||
true
|
||||
end
|
||||
|
||||
def exists?
|
||||
file.try(:exists?)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
To add the entry manually, provide the following details to the application on your phone.
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Account:
|
||||
= current_user.email
|
||||
= @account_string
|
||||
%p.prepend-top-0.append-bottom-0
|
||||
Key:
|
||||
= current_user.otp_secret.scan(/.{4}/).join(' ')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- @content_class = "limit-container-width"
|
||||
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
|
||||
- page_description @issue.description
|
||||
- page_card_attributes @issue.card_attributes
|
||||
|
|
|
@ -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"
|
||||
|
@ -39,7 +39,7 @@
|
|||
= icon('thumbs-down')
|
||||
= downvotes
|
||||
|
||||
- note_count = merge_request.mr_and_commit_notes.user.count
|
||||
- note_count = merge_request.related_notes.user.count
|
||||
%li
|
||||
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
|
||||
= icon('comments')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- @content_class = "limit-container-width"
|
||||
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
|
||||
- page_description @merge_request.description
|
||||
- page_card_attributes @merge_request.card_attributes
|
||||
|
@ -41,7 +42,7 @@
|
|||
= 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)
|
||||
.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
|
||||
= succeed '.' do
|
||||
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
|
||||
|
@ -53,7 +54,7 @@
|
|||
%li.notes-tab
|
||||
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
|
||||
Discussion
|
||||
%span.badge= @merge_request.mr_and_commit_notes.user.count
|
||||
%span.badge= @merge_request.related_notes.user.count
|
||||
- if @merge_request.source_project
|
||||
%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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add a slug to environments
|
||||
merge_request: 7983
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add focus state to dropdown items
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Improve bulk assignment for issuables
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Improve help message for issue create slash command
|
||||
merge_request: 7850
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Added go back anchor on error pages.
|
||||
merge_request: 8087
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 25617 Fix placeholder color of todo filters
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
|
||||
merge_request: 8003
|
||||
author: Munken
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Replace static fixture for abuse_reports_spec
|
||||
merge_request: 7644
|
||||
author: winniehell
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add GitLab host to 2FA QR code and manual info
|
||||
merge_request: 6941
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
|
||||
merge_request:
|
||||
author: twonegatives
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'Gem update: Update grape to 0.18.0'
|
||||
merge_request:
|
||||
author: Robert Schilling
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Replace Rack::Multipart with GitLab-Workhorse based solution
|
||||
merge_request: 5867
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Create mattermost service
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix missing Note access checks by moving Note#search to updated NoteFinder
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Move admin active tab spinach tests to rspec
|
||||
merge_request: 8037
|
||||
author: Semyon Pupkov
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Remove unnecessary commits order message
|
||||
merge_request: 8004
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Show commit status from latest pipeline
|
||||
merge_request: 7333
|
||||
author:
|
|
@ -45,7 +45,7 @@ module Gitlab
|
|||
#
|
||||
# Parameters filtered:
|
||||
# - Password (:password, :password_confirmation)
|
||||
# - Private tokens (:private_token, :authentication_token)
|
||||
# - Private tokens
|
||||
# - Two-factor tokens (:otp_attempt)
|
||||
# - Repo/Project Import URLs (:import_url)
|
||||
# - Build variables (:variables)
|
||||
|
@ -60,11 +60,13 @@ module Gitlab
|
|||
encrypted_key
|
||||
hook
|
||||
import_url
|
||||
incoming_email_token
|
||||
key
|
||||
otp_attempt
|
||||
password
|
||||
password_confirmation
|
||||
private_token
|
||||
runners_token
|
||||
secret_token
|
||||
sentry_dsn
|
||||
variables
|
||||
|
@ -85,6 +87,8 @@ module Gitlab
|
|||
config.assets.precompile << "print.css"
|
||||
config.assets.precompile << "notify.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 << "users/users_bundle.js"
|
||||
config.assets.precompile << "network/network_bundle.js"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Touch the lexers so it is registered with Rouge
|
||||
Rouge::Lexers::Math
|
|
@ -0,0 +1,3 @@
|
|||
Rails.application.configure do |config|
|
||||
config.middleware.use(Gitlab::Middleware::Multipart)
|
||||
end
|
|
@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
|
|||
|
||||
def job_attributes(pipeline, opts)
|
||||
{ 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
|
||||
}.merge(opts)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
# rubocop:disable all
|
||||
class MoveSlackServiceToWebhook < ActiveRecord::Migration
|
||||
|
||||
DOWNTIME = true
|
||||
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
|
||||
|
||||
def change
|
||||
SlackService.all.each do |slack_service|
|
||||
SlackNotificationService.all.each do |slack_service|
|
||||
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
|
||||
token = slack_service.properties['token']
|
||||
subdomain = slack_service.properties['subdomain']
|
||||
|
|
|
@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
|
|||
end
|
||||
|
||||
def down
|
||||
Route.delete_all(source_type: 'Namespace')
|
||||
execute("DELETE FROM routes WHERE source_type = 'Namespace'")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
|
|||
DOWNTIME_REASON = 'No new projects should be created during data copy'
|
||||
|
||||
def up
|
||||
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
|
||||
if Gitlab::Database.postgresql?
|
||||
execute <<-EOF
|
||||
INSERT INTO routes (source_id, source_type, path)
|
||||
(SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
|
||||
FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
|
||||
ORDER BY namespaces.path, projects.path, projects.id DESC)
|
||||
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
|
||||
|
||||
def down
|
||||
Route.delete_all(source_type: 'Project')
|
||||
execute("DELETE FROM routes WHERE source_type = 'Project'")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
|
|||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row|
|
||||
path = connection.quote(row['path'])
|
||||
execute(%Q{
|
||||
DELETE FROM #{quote_table_name(:routes)}
|
||||
WHERE path = #{path}
|
||||
AND id != (
|
||||
SELECT id FROM (
|
||||
SELECT max(id) AS id
|
||||
FROM #{quote_table_name(:routes)}
|
||||
WHERE path = #{path}
|
||||
) max_ids
|
||||
)
|
||||
})
|
||||
end
|
||||
# We can skip this migration when running a PostgreSQL database because
|
||||
# we use an optimized query in the "FillProjectsRoutesTable" migration
|
||||
# to fill these values that avoid duplicate entries in the routes table.
|
||||
return unless Gitlab::Database.mysql?
|
||||
|
||||
execute <<-EOF
|
||||
DELETE duplicated_rows.*
|
||||
FROM routes AS duplicated_rows
|
||||
INNER JOIN (
|
||||
SELECT path, MAX(id) as max_id
|
||||
FROM routes
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1
|
||||
) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
|
||||
EOF
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration
|
|||
end
|
||||
|
||||
def down
|
||||
if index_exists?(:namespaces, :name)
|
||||
if index_exists?(:namespaces, [:name, :parent_id])
|
||||
remove_index :namespaces, [:name, :parent_id]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue