Commit a9d9895d authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Merge branch 'master' into 'zj-mattermost-slash-config'

# Conflicts:
#   config/gitlab.yml.example
#   lib/mattermost/session.rb
#   spec/lib/mattermost/session_spec.rb
parents b23f32a7 546fa165
......@@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
Enabled: false
Enabled: true
EnforcedStyle: indented
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
......
......@@ -22,7 +22,6 @@ gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
......@@ -67,7 +66,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'
......
......@@ -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)
......@@ -428,10 +432,6 @@ GEM
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-bitbucket (0.0.2)
multi_json (~> 1.7)
omniauth (~> 1.1)
omniauth-oauth (~> 1.0)
omniauth-cas3 (1.1.3)
addressable (~> 2.3)
nokogiri (~> 1.6.6)
......@@ -505,14 +505,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 +741,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 +860,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)
......@@ -899,7 +898,6 @@ DEPENDENCIES
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.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;
......
......@@ -449,7 +449,7 @@
</span>
</td>
<td>
<td class="environments-build-cell">
<a v-if="shouldRenderBuildName"
class="build-link"
:href="model.last_deployment.deployable.build_path">
......
/* 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;
};
}
......@@ -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() {
......
......@@ -30,7 +30,7 @@
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
......
......@@ -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;
},
......
......@@ -19,7 +19,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
GitLab.GfmAutoComplete.setup();
gl.GfmAutoComplete.setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
......
......@@ -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()));
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 = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return labelIds;
return _.intersection.apply(this, labelIds);
}
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
}
/**
* Returns Label IDs that will be removed from issue selection
* @return {Array} Array of labels IDs
*/
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
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);
// 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);
}
});
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 @@
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,34 +173,41 @@
});
},
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) {
} 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;
// Reduce the colors to 4
......@@ -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);
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
/* global MergeRequestTabs */
/*= require jquery.waitforimages */
......@@ -27,6 +27,7 @@
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
this.initCommitMessageListeners();
if ($("a.btn-close").length) {
this.initTaskList();
}
......@@ -108,6 +109,26 @@
// note so that we can re-use its form here
};
MergeRequest.prototype.initCommitMessageListeners = function() {
var textarea = $('textarea.js-commit-message');
$('a.js-with-description-link').on('click', function(e) {
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
$('p.js-with-description-hint').hide();
$('p.js-without-description-hint').show();
});
$('a.js-without-description-link').on('click', function(e) {
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
$('p.js-with-description-hint').show();
$('p.js-without-description-hint').hide();
});
};
return MergeRequest;
})();
......
......@@ -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));
......
/* 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 @@
// <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);
......@@ -98,7 +98,7 @@
@extend .dropdown-toggle;
padding-right: 20px;
position: relative;
width: 160px;
width: 163px;
text-overflow: ellipsis;
overflow: hidden;
......@@ -188,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
outline: 0;
}
&.dropdown-menu-empty-link {
......
......@@ -26,6 +26,45 @@ body {
.container-limited {
max-width: $fixed-layout-width;
&.limit-container-width {
max-width: $limited-layout-width;
}
}
.alert-wrapper {
margin-bottom: $gl-padding;
.alert {
margin-bottom: 0;
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
background-color: lighten($gl-warning, 4%);
border-color: lighten($gl-warning, 4%);
}
.alert-warning + .alert-warning {
background-color: $gl-warning;
border-color: $gl-warning;
}
.alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 4%);
border-color: darken($gl-warning, 4%);
}
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
background-color: darken($gl-warning, 8%);
border-color: darken($gl-warning, 8%);
}
.alert-warning:only-of-type {
background-color: $gl-warning;
border-color: $gl-warning;
}
}
......
......@@ -35,7 +35,7 @@
@import "bootstrap/alerts";
// @import "bootstrap/progress-bars";
@import "bootstrap/list-group";
// @import "bootstrap/wells";
@import "bootstrap/wells";
@import "bootstrap/close";
@import "bootstrap/panels";
......
......@@ -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;
......@@ -436,7 +438,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 14px;
$label-border-radius: 100px;
/*
* Lint
......@@ -472,7 +474,6 @@ $project-option-descr-color: #54565b;
$project-breadcrumb-color: #999;
$project-private-forks-notice-odd: #2aa056;
$project-network-controls-color: #888;
$project-limit-message-bg: #f28d35;
/*
* Runners
......@@ -522,3 +523,9 @@ $body-text-shadow: rgba(255,255,255,0.01);
*/
$ui-dev-kit-example-color: #bbb;
$ui-dev-kit-example-border: #ddd;
/*
Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
......@@ -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%;
}
}
}
}
}
......@@ -30,19 +30,25 @@
display: table-cell;
}
.environments-name,
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-deploy,
.environments-build,
.environments-date {
width: 10%;
}
.environments-name {
width: 30%;
.environments-deploy,
.environments-build {
width: 15%;
}
.environment-name,
.environments-build-cell,
.deployment-column {
word-break: break-all;
}
.deployment-column {
......
// 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);
}
}
......
......@@ -98,7 +98,7 @@
}
.label {
padding: 9px;
padding: 8px 9px 9px;
font-size: 14px;
}
}
......@@ -201,6 +201,8 @@
.label-remove {
border-left: 1px solid $label-remove-border;
z-index: 3;
border-radius: $label-border-radius;
padding: 6px 10px 6px 9px;
}
.btn {
......
......@@ -78,6 +78,21 @@
float: right;
}
.dropdown {
width: 100%;
margin-top: 5px;
.dropdown-menu-toggle {
vertical-align: middle;
width: 100%;
}
@media (min-width: $screen-sm-min) {
margin-top: 0;
width: 155px;
}
}
.form-control {
width: 100%;
padding-right: 35px;
......@@ -85,12 +100,22 @@
@media (min-width: $screen-sm-min) {
width: 350px;
}
&.input-short {
@media (min-width: $screen-md-min) {
width: 170px;
}
@media (min-width: $screen-lg-min) {
width: 210px;
}
}
}
}
.member-search-btn {
position: absolute;
right: 0;
right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
......@@ -99,4 +124,8 @@
background: transparent;
border: 0;
outline: 0;
@media (min-width: $screen-sm-min) {
right: 160px;
}
}
......@@ -383,10 +383,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions {
......
This diff is collapsed.
......@@ -262,3 +262,13 @@ table.u2f-registrations {
border-right: solid 1px transparent;
}
}
.oauth-application-show {
.scope-name {
font-weight: 600;
}
.scopes-list {
padding-left: 18px;
}
}
\ No newline at end of file
......@@ -6,12 +6,6 @@
}
}
.no-ssh-key-message,
.project-limit-message {
background-color: $project-limit-message-bg;
margin-bottom: 0;
}
.new_project,
.edit-project {
......
class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
before_action :load_scopes, only: [:new, :edit]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
......@@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
params[:doorkeeper_application].permit(:name, :redirect_uri)
params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
end
end
......@@ -262,7 +262,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
Gitlab::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
......
module OauthApplications
extend ActiveSupport::Concern
included do
before_action :prepare_scopes, only: [:create, :update]
end
def prepare_scopes
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
if scopes
params[:doorkeeper_application][:scopes] = scopes.join(' ')
end
end
def load_scopes
@scopes = Doorkeeper.configuration.scopes
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@members = @members.where(user_id: users)
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
......
......@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
rescue_from OAuth::Error, with: :bitbucket_unauthorized
rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
rescue_from OAuth2::Error, with: :bitbucket_unauthorized
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
request_token = session.delete(:oauth_request_token)
raise "Session expired!" if request_token.nil?
response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
request_token.symbolize_keys!
access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
session[:bitbucket_access_token] = access_token.token
session[:bitbucket_access_token_secret] = access_token.secret
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
session[:bitbucket_expires_in] = response.expires_in
session[:bitbucket_refresh_token] = response.refresh_token
redirect_to status_import_bitbucket_url
end
def status
@repos = client.projects
@incompatible_repos = client.incompatible_projects
bitbucket_client = Bitbucket::Client.new(credentials)
repos = bitbucket_client.repos
@already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
@already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
render json: jobs
render json: current_user.created_projects
.where(import_type: 'bitbucket')
.to_json(only: [:id, :import_status])
end
def create
bitbucket_client = Bitbucket::Client.new(credentials)
@repo_id = params[:repo_id].to_s
repo = client.project(@repo_id.gsub('___', '/'))
@project_name = repo['slug']
@target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
name = @repo_id.gsub('___', '/')
repo = bitbucket_client.repo(name)
@project_name = params[:new_name].presence || repo.name
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
render 'deploy_key' and return
end
repo_owner = repo.owner
repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
@target_namespace = params[:new_namespace].presence || repo_owner
namespace = find_or_create_namespace(@target_namespace, current_user)
if current_user.can?(:create_projects, @target_namespace)
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
if current_user.can?(:create_projects, namespace)
# The token in a session can be expired, we need to get most recent one because
# Bitbucket::Connection class refreshes it.
session[:bitbucket_token] = bitbucket_client.connection.token
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
else
render 'unauthorized'
end
......@@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
private
def client
@client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
session[:bitbucket_access_token_secret])
@client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
end
def provider
Gitlab::OAuth::Provider.config_for('bitbucket')
end
def options
OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
def verify_bitbucket_import_enabled
......@@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
end
def bitbucket_auth
if session[:bitbucket_access_token].blank?
go_to_bitbucket_for_permissions
end
go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
end
def go_to_bitbucket_for_permissions
request_token = client.request_token(callback_import_bitbucket_url)
session[:oauth_request_token] = request_token
redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
end
def bitbucket_unauthorized
go_to_bitbucket_for_permissions
end
def access_params
def credentials
{
bitbucket_access_token: session[:bitbucket_access_token],
bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
token: session[:bitbucket_token],
expires_at: session[:bitbucket_expires_at],
expires_in: session[:bitbucket_expires_in],
refresh_token: session[:bitbucket_refresh_token]
}
end
end
......@@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
include OauthApplications
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
before_action :add_gon_variables
before_action :load_scopes, only: [:index, :create, :edit]
layout 'profile'
......
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
before_action :load_personal_access_tokens, only: :index
def index
@personal_access_token = current_user.personal_access_tokens.build
set_index_vars
end
def create
......@@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
load_personal_access_tokens
set_index_vars
render :index
end
end
......@@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at)
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
def load_personal_access_tokens
def set_index_vars
@personal_access_token ||= current_user.personal_access_tokens.build
@scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
......
......@@ -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
......
class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:emojis, :members]
def emojis
render json: Gitlab::AwardEmoji.urls
end
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
end
def issues
render json: @autocomplete_service.issues
end
def merge_requests
render json: @autocomplete_service.merge_requests
end
def labels
render json: @autocomplete_service.labels
end
def milestones
render json: @autocomplete_service.milestones
end
def commands
render json: @autocomplete_service.commands(noteable, params[:type])
end
private
def load_autocomplete_service
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
end
def noteable
case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
end
end
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
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
include SortingHelper
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
......@@ -35,12 +37,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
order(access_level: :desc).page(params[:page])
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
......
......@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), alert: ex.message
end
def autocomplete_sources
noteable =
case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
else
nil
end
autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
@suggestions = {
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants,
commands: autocomplete.commands(noteable, params[:type])
}
respond_to do |format|
format.json { render json: @suggestions }
end
end
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
......
......@@ -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
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
def execute
@notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
@notes
end
notes =
case target_type
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
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).find(target_id).notes.inc_author
IssuesFinder.new(@current_user, project_id: @project.id).execute
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"
project.snippets.find(target_id).notes
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
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
......@@ -12,11 +12,18 @@ module GroupsHelper
end
def group_title(group, name = nil, url = nil)
full_title = link_to(simple_sanitize(group.name), group_path(group))
full_title = ''
group.parents.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do
full_title
full_title.html_safe
end
end
......
......@@ -36,4 +36,12 @@ module MembersHelper
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
def filter_group_project_member_path(options = {})
options = params.slice(:search, :sort).merge(options)
path = request.path
path << "?#{options.to_param}"
path
end
end
......@@ -59,6 +59,10 @@ module MergeRequestsHelper
@mr_closes_issues ||= @merge_request.closes_issues
end
def mr_issues_mentioned_but_not_closing
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
end
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
......
......@@ -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))
......
......@@ -25,7 +25,7 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_oldest_created => sort_title_oldest_created
}
if current_controller?('admin/projects')
......@@ -35,6 +35,19 @@ module SortingHelper
options
end
def member_sort_options_hash
{
sort_value_access_level_asc => sort_title_access_level_asc,
sort_value_access_level_desc => sort_title_access_level_desc,
sort_value_last_joined => sort_title_last_joined,
sort_value_oldest_joined => sort_title_oldest_joined,
sort_value_name => sort_title_name_asc,
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin
}
end
def sort_title_priority
'Priority'
end
......@@ -95,6 +108,50 @@ module SortingHelper
'Most popular'
end
def sort_title_last_joined
'Last joined'
end
def sort_title_oldest_joined
'Oldest joined'
end
def sort_title_access_level_asc
'Access level, ascending'
end
def sort_title_access_level_desc
'Access level, descending'
end
def sort_title_name_asc
'Name, ascending'
end
def sort_title_name_desc
'Name, descending'
end
def sort_value_last_joined
'last_joined'
end
def sort_value_oldest_joined
'oldest_joined'
end
def sort_value_access_level_asc
'access_level_asc'
end
def sort_value_access_level_desc
'access_level_desc'
end
def sort_value_name_desc
'name_desc'
end
def sort_value_priority
'priority'
end
......
......@@ -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,11 +151,11 @@ module Ci
end
def expanded_environment_name
ExpandVariables.expand(environment, variables) if environment
ExpandVariables.expand(environment, simple_variables) if environment
end
def has_environment?
self.environment.present?
environment.present?
end
def starts_environment?
......@@ -206,12 +214,14 @@ 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
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
variables += project.secret_variables
......@@ -219,6 +229,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
......
......@@ -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?
......
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
......
......@@ -57,6 +57,11 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
......@@ -72,6 +77,34 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
def search(query)
joins(:user).merge(User.search(query))
end
def sort(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
when 'access_level_desc' then reorder(access_level: :desc)
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
when 'last_joined' then order_created_desc
when 'oldest_joined' then order_created_asc
else
order_by(method)
end
end
def left_join_users
users = User.arel_table
members = Member.arel_table
member_users = members.join(users, Arel::Nodes::OuterJoin).
on(members[:user_id].eq(users[:id])).
join_sources
joins(member_users)
end
def access_for_user_ids(user_ids)
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
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
......@@ -568,6 +568,19 @@ class MergeRequest < ActiveRecord::Base
end
end
def issues_mentioned_but_not_closing(current_user = self.author)
return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description)
issues = ext.issues
closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description)
issues - closing_issues
end
def target_project_path
if target_project
target_project.path_with_namespace
......@@ -612,13 +625,24 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_names.include?(self.target_branch)
end
def merge_commit_message
message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
message << "#{title}\n\n"
message << "#{description}\n\n" if description.present?
def merge_commit_message(include_description: false)
closes_issues_references = closes_issues.map do |issue|
issue.to_reference(target_project)
end
message = [
"Merge branch '#{source_branch}' into '#{target_branch}'",
title
]
if !include_description && closes_issues_references.present?
message << "Closes #{closes_issues_references.to_sentence}"
end
message << "#{description}" if include_description && description.present?
message << "See merge request #{to_reference}"
message
message.join("\n\n")
end
def reset_merge_when_build_succeeds
......
......@@ -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?
......
......@@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
serialize :scopes, Array
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
......
......@@ -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
......@@ -1229,6 +1230,12 @@ class Project < ActiveRecord::Base
end
end
def deployment_variables
return [] unless deployment_service
deployment_service.predefined_variables
end
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s)
......
require 'slack-notifier'
class SlackService
module ChatMessage
class BaseMessage
def initialize(params)
raise NotImplementedError
......
class SlackService
module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type
......
class SlackService
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title
......
class SlackService
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name
......
class SlackService
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name
......
class SlackService
module ChatMessage
class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
......
class SlackService
module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before
......
class SlackService
module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :user_name
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
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[: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)
true
else
false
end
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"
# 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'
......
......@@ -8,4 +8,8 @@ class DeploymentService < Service
def supported_events
[]
end
def predefined_variables
[]
end
end
......@@ -83,6 +83,16 @@ class KubernetesService < DeploymentService
{ success: false, result: err }
end
def predefined_variables
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true }
]
variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
variables
end
private
def build_kubeclient(api_path = '/api', api_version = 'v1')
......
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
pivotaltracker
pushover
redmine
slack
mattermost_notification
slack_notification
teamcity
]
end
......
......@@ -178,6 +178,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
......@@ -205,8 +207,8 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
end
......
AccessTokenValidationService = Struct.new(:token) do
# Results:
VALID = :valid
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
def validate(scopes: [])
if token.expired?
return EXPIRED
elsif token.revoked?
return REVOKED
elsif !self.include_any_scope?(scopes)
return INSUFFICIENT_SCOPE
else
return VALID
end
end
# True if the token's scope contains any of the passed scopes.
def include_any_scope?(scopes)
if scopes.blank?
true
else
# Check whether the token is allowed access to any of the required scopes.
Set.new(scopes).intersection(Set.new(token.scopes)).present?
end
end
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
......
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
......
......@@ -184,7 +184,8 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params)
# We do not touch as it will affect a update on updated_at field
......@@ -201,6 +202,10 @@ class IssuableBaseService < BaseService
issuable
end
def labels_changing?(old_label_ids, new_label_ids)
old_label_ids.sort != new_label_ids.sort
end
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
......
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.
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