Commit 678a6698 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-geo-show-event-log-data

parents d532f659 e5e3f88e
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 9.5.3 (2017-09-03)
- [FIXED] Check if table exists before loading the current license. !2783
- [FIXED] Extend early adopters feature set.
## 9.5.2 (2017-08-28) ## 9.5.2 (2017-08-28)
- [FIXED] Fix LDAP backwards-compatibility when using "method" or when "verify_certificates" is not defined. !2690 - [FIXED] Fix LDAP backwards-compatibility when using "method" or when "verify_certificates" is not defined. !2690
......
...@@ -2,6 +2,24 @@ ...@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.5.3 (2017-09-03)
- [SECURITY] Filter additional secrets from Rails logs.
- [FIXED] Make username update fail if the namespace update fails. !13642
- [FIXED] Fix failure when issue is authored by a deleted user. !13807
- [FIXED] Reverts changes made to signin_enabled. !13956
- [FIXED] Fix Merge when pipeline succeeds button dropdown caret icon horizontal alignment.
- [FIXED] Fixed diff changes bar buttons from showing/hiding whilst scrolling.
- [FIXED] Fix events error importing GitLab projects.
- [FIXED] Fix pipeline trigger via API fails with 500 Internal Server Error in 9.5.
- [FIXED] Fixed fly-out nav flashing in & out.
- [FIXED] Remove closing external issues by reference error.
- [FIXED] Re-allow appearances.description_html to be NULL.
- [CHANGED] Update and fix resolvable note icons for easier recognition.
- [OTHER] Eager load head pipeline projects for MRs index.
- [OTHER] Instrument MergeRequest#fetch_ref.
- [OTHER] Instrument MergeRequest#ensure_ref_fetched.
## 9.5.2 (2017-08-28) ## 9.5.2 (2017-08-28)
- [FIXED] Fix signing in using LDAP when attribute mapping uses simple strings instead of arrays. - [FIXED] Fix signing in using LDAP when attribute mapping uses simple strings instead of arrays.
......
...@@ -752,7 +752,7 @@ GEM ...@@ -752,7 +752,7 @@ GEM
retriable (1.4.1) retriable (1.4.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (2.2.0) rouge (2.2.1)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
......
...@@ -486,7 +486,7 @@ GitLabDropdown = (function() { ...@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.shouldPropagate = function(e) { GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target; var $target;
if (this.options.multiSelect) { if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target); $target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') && if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') && !$target.hasClass('dropdown-menu-close-icon') &&
...@@ -546,10 +546,10 @@ GitLabDropdown = (function() { ...@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
}; };
GitLabDropdown.prototype.positionMenuAbove = function() { GitLabDropdown.prototype.positionMenuAbove = function() {
var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu'); var $menu = this.dropdown.find('.dropdown-menu');
$menu.css('top', ($button.height() + $menu.height()) * -1); $menu.css('top', 'initial');
$menu.css('bottom', '100%');
}; };
GitLabDropdown.prototype.hidden = function(e) { GitLabDropdown.prototype.hidden = function(e) {
...@@ -713,7 +713,7 @@ GitLabDropdown = (function() { ...@@ -713,7 +713,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.noResults = function() { GitLabDropdown.prototype.noResults = function() {
var html; var html;
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>';
}; };
GitLabDropdown.prototype.rowClicked = function(el) { GitLabDropdown.prototype.rowClicked = function(el) {
......
...@@ -11,8 +11,6 @@ import ZenMode from './zen_mode'; ...@@ -11,8 +11,6 @@ import ZenMode from './zen_mode';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) { function IssuableForm(form) {
...@@ -28,7 +26,6 @@ import ZenMode from './zen_mode'; ...@@ -28,7 +26,6 @@ import ZenMode from './zen_mode';
new ZenMode(); new ZenMode();
this.titleField = this.form.find("input[name*='[title]']"); this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']"); this.descriptionField = this.form.find("textarea[name*='[description]']");
this.issueMoveField = this.form.find("#move_to_project_id");
if (!(this.titleField.length && this.descriptionField.length)) { if (!(this.titleField.length && this.descriptionField.length)) {
return; return;
} }
...@@ -36,7 +33,6 @@ import ZenMode from './zen_mode'; ...@@ -36,7 +33,6 @@ import ZenMode from './zen_mode';
this.form.on("submit", this.handleSubmit); this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave); this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip(); this.initWip();
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date'); $issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) { if ($issuableDueDate.length) {
calendar = new Pikaday({ calendar = new Pikaday({
...@@ -58,12 +54,6 @@ import ZenMode from './zen_mode'; ...@@ -58,12 +54,6 @@ import ZenMode from './zen_mode';
}; };
IssuableForm.prototype.handleSubmit = function() { IssuableForm.prototype.handleSubmit = function() {
var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
if ((parseInt(fieldId, 10) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
}
return this.resetAutosave(); return this.resetAutosave();
}; };
...@@ -115,48 +105,6 @@ import ZenMode from './zen_mode'; ...@@ -115,48 +105,6 @@ import ZenMode from './zen_mode';
return this.titleField.val("WIP: " + (this.titleField.val())); return this.titleField.val("WIP: " + (this.titleField.val()));
}; };
IssuableForm.prototype.initMoveDropdown = function() {
var $moveDropdown, pageSize;
$moveDropdown = $('.js-move-dropdown');
if ($moveDropdown.length) {
pageSize = $moveDropdown.data('page-size');
return $('.js-move-dropdown').select2({
ajax: {
url: $moveDropdown.data('projects-url'),
quietMillis: 125,
data: function(term, page, context) {
return {
search: term,
offset_id: context
};
},
results: function(data) {
var context,
more;
if (data.length >= pageSize)
more = true;
if (data[data.length - 1])
context = data[data.length - 1].id;
return {
results: data,
more: more,
context: context
};
}
},
formatResult: function(project) {
return project.name_with_namespace;
},
formatSelection: function(project) {
return project.name_with_namespace;
}
});
}
};
return IssuableForm; return IssuableForm;
})(); })();
}).call(window); }).call(window);
...@@ -17,10 +17,6 @@ export default { ...@@ -17,10 +17,6 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
canMove: {
required: true,
type: Boolean,
},
canUpdate: { canUpdate: {
required: true, required: true,
type: Boolean, type: Boolean,
...@@ -96,10 +92,6 @@ export default { ...@@ -96,10 +92,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectsAutocompleteUrl: {
type: String,
required: true,
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
...@@ -142,7 +134,6 @@ export default { ...@@ -142,7 +134,6 @@ export default {
confidential: this.isConfidential, confidential: this.isConfidential,
description: this.state.descriptionText, description: this.state.descriptionText,
lockedWarningVisible: false, lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false, updateLoading: false,
}); });
} }
...@@ -151,16 +142,6 @@ export default { ...@@ -151,16 +142,6 @@ export default {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { updateIssuable() {
const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
if (!canPostUpdate) {
this.store.setFormState({
updateLoading: false,
});
return;
}
this.service.updateIssuable(this.store.formState) this.service.updateIssuable(this.store.formState)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
...@@ -239,14 +220,12 @@ export default { ...@@ -239,14 +220,12 @@ export default {
<form-component <form-component
v-if="canUpdate && showForm" v-if="canUpdate && showForm"
:form-state="formState" :form-state="formState"
:can-move="canMove"
:can-destroy="canDestroy" :can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
:markdown-docs="markdownDocs" :markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl" :markdown-preview-url="markdownPreviewUrl"
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:projects-autocomplete-url="projectsAutocompleteUrl"
/> />
<div v-else> <div v-else>
<title-component <title-component
......
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
formState: {
type: Object,
required: true,
},
projectsAutocompleteUrl: {
type: String,
required: true,
},
},
mounted() {
const $moveDropdown = $(this.$refs['move-dropdown']);
$moveDropdown.select2({
ajax: {
url: this.projectsAutocompleteUrl,
quietMillis: 125,
data(term, page, context) {
return {
search: term,
offset_id: context,
};
},
results(data) {
const more = data.length >= 50;
const context = data[data.length - 1] ? data[data.length - 1].id : null;
return {
results: data,
more,
context,
};
},
},
formatResult(project) {
return project.name_with_namespace;
},
formatSelection(project) {
return project.name_with_namespace;
},
})
.on('change', (e) => {
this.formState.move_to_project_id = parseInt(e.target.value, 10);
});
},
beforeDestroy() {
$(this.$refs['move-dropdown']).select2('destroy');
},
};
</script>
<template>
<fieldset>
<label
for="issuable-move"
class="sr-only">
Move
</label>
<div class="issuable-form-select-holder append-right-5">
<input
ref="move-dropdown"
type="hidden"
id="issuable-move"
data-placeholder="Move to a different project" />
</div>
<span
v-tooltip
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
<i
class="fa fa-question-circle"
aria-hidden="true">
</i>
</span>
</fieldset>
</template>
...@@ -4,15 +4,10 @@ ...@@ -4,15 +4,10 @@
import descriptionField from './fields/description.vue'; import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue'; import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue'; import descriptionTemplate from './fields/description_template.vue';
import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue'; import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default { export default {
props: { props: {
canMove: {
type: Boolean,
required: true,
},
canDestroy: { canDestroy: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -42,10 +37,6 @@ ...@@ -42,10 +37,6 @@
type: String, type: String,
required: true, required: true,
}, },
projectsAutocompleteUrl: {
type: String,
required: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
...@@ -53,7 +44,6 @@ ...@@ -53,7 +44,6 @@
descriptionField, descriptionField,
descriptionTemplate, descriptionTemplate,
editActions, editActions,
projectMove,
confidentialCheckbox, confidentialCheckbox,
}, },
computed: { computed: {
...@@ -93,10 +83,6 @@ ...@@ -93,10 +83,6 @@
:markdown-docs="markdownDocs" /> :markdown-docs="markdownDocs" />
<confidential-checkbox <confidential-checkbox
:form-state="formState" /> :form-state="formState" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-url="projectsAutocompleteUrl" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy" />
......
...@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props: { props: {
canUpdate: this.canUpdate, canUpdate: this.canUpdate,
canDestroy: this.canDestroy, canDestroy: this.canDestroy,
canMove: this.canMove,
endpoint: this.endpoint, endpoint: this.endpoint,
issuableRef: this.issuableRef, issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml, initialTitleHtml: this.initialTitleHtml,
...@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
markdownDocs: this.markdownDocs, markdownDocs: this.markdownDocs,
projectPath: this.projectPath, projectPath: this.projectPath,
projectNamespace: this.projectNamespace, projectNamespace: this.projectNamespace,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
......
...@@ -6,7 +6,6 @@ export default class Store { ...@@ -6,7 +6,6 @@ export default class Store {
confidential: false, confidential: false,
description: '', description: '',
lockedWarningVisible: false, lockedWarningVisible: false,
move_to_project_id: 0,
updateLoading: false, updateLoading: false,
}; };
} }
......
...@@ -2,19 +2,20 @@ import _ from 'underscore'; ...@@ -2,19 +2,20 @@ import _ from 'underscore';
(() => { (() => {
/* /*
* TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
* stringifyTime condensed or non-condensed, abbreviateTimelengths) * non-condensed, abbreviateTimelengths)
* */ * */
const utils = window.gl.utils = gl.utils || {}; const utils = window.gl.utils = gl.utils || {};
const prettyTime = utils.prettyTime = { const prettyTime = utils.prettyTime = {
/* /*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero. * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length.
*/ */
parseSeconds(seconds) { parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
const DAYS_PER_WEEK = 5; const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = 8; const HOURS_PER_DAY = hoursPerDay;
const MINUTES_PER_HOUR = 60; const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
......
...@@ -65,10 +65,6 @@ import Cookies from 'js-cookie'; ...@@ -65,10 +65,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val()); return _this.changeProject($(e.currentTarget).val());
}; };
})(this)); })(this));
return $('.js-projects-dropdown-toggle').on('click', function(e) {
e.preventDefault();
return $('.js-projects-dropdown').select2('open');
});
}; };
Project.prototype.changeProject = function(url) { Project.prototype.changeProject = function(url) {
......
...@@ -5,51 +5,7 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -5,51 +5,7 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function () { (function () {
this.ProjectSelect = (function () { this.ProjectSelect = (function () {
function ProjectSelect() { function ProjectSelect() {
$('.js-projects-dropdown-toggle').each(function (i, dropdown) { $('.ajax-project-select').each(function(i, select) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace']
},
data: function (term, callback) {
var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function (projects) {
return callback(projects);
};
if (this.includeGroups) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
return Api.projects(term, {
order_by: orderBy
}, projectsCallback);
}
},
url: function (project) {
return project.web_url;
},
text: function (project) {
return project.name_with_namespace;
}
});
});
$('.ajax-project-select').each(function (i, select) {
var placeholder; var placeholder;
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups'); this.includeGroups = $(select).data('include-groups');
......
...@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager'; ...@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar.prototype.openDropdown = function(blockOrName) { Sidebar.prototype.openDropdown = function(blockOrName) {
var $block; var $block;
$block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
$block.find('.edit-link').trigger('click');
if (!this.isOpen()) { if (!this.isOpen()) {
this.setCollapseAfterUpdate($block); this.setCollapseAfterUpdate($block);
return this.toggleSidebar('open'); this.toggleSidebar('open');
} }
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout(() => {
$block.find('.js-sidebar-dropdown-toggle').trigger('click');
});
}; };
Sidebar.prototype.setCollapseAfterUpdate = function($block) { Sidebar.prototype.setCollapseAfterUpdate = function($block) {
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
/> />
<a <a
v-if="editable" v-if="editable"
class="edit-link pull-right" class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#" href="#"
> >
Edit Edit
......
/* global Flash */
function isValidProjectId(id) {
return id > 0;
}
class SidebarMoveIssue {
constructor(mediator, dropdownToggle, confirmButton) {
this.mediator = mediator;
this.$dropdownToggle = $(dropdownToggle);
this.$confirmButton = $(confirmButton);
this.onConfirmClickedWrapper = this.onConfirmClicked.bind(this);
}
init() {
this.initDropdown();
this.addEventListeners();
}
destroy() {
this.removeEventListeners();
}
initDropdown() {
this.$dropdownToggle.glDropdown({
search: {
fields: ['name_with_namespace'],
},
showMenuAbove: true,
selectable: true,
filterable: true,
filterRemote: true,
multiSelect: false,
// Keep the dropdown open after selecting an option
shouldPropagate: false,
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new Flash('An error occured while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
${project.name_with_namespace}
</a>
</li>
`,
clicked: (options) => {
const project = options.selectedObj;
const selectedProjectId = options.isMarking ? project.id : 0;
this.mediator.setMoveToProjectId(selectedProjectId);
this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId));
},
});
}
addEventListeners() {
this.$confirmButton.on('click', this.onConfirmClickedWrapper);
}
removeEventListeners() {
this.$confirmButton.off('click', this.onConfirmClickedWrapper);
}
onConfirmClicked() {
if (isValidProjectId(this.mediator.store.moveToProjectId)) {
this.$confirmButton
.disable()
.addClass('is-loading');
this.mediator.moveIssue()
.catch(() => {
Flash('An error occured while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
});
}
}
}
export default SidebarMoveIssue;
...@@ -4,9 +4,11 @@ import VueResource from 'vue-resource'; ...@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue.use(VueResource); Vue.use(VueResource);
export default class SidebarService { export default class SidebarService {
constructor(endpoint) { constructor(endpointMap) {
if (!SidebarService.singleton) { if (!SidebarService.singleton) {
this.endpoint = endpoint; this.endpoint = endpointMap.endpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
SidebarService.singleton = this; SidebarService.singleton = this;
} }
...@@ -25,4 +27,18 @@ export default class SidebarService { ...@@ -25,4 +27,18 @@ export default class SidebarService {
emulateJSON: true, emulateJSON: true,
}); });
} }
getProjectsAutocomplete(searchTerm) {
return Vue.http.get(this.projectsAutocompleteEndpoint, {
params: {
search: searchTerm,
},
});
}
moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
});
}
} }
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees'; import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue'; import confidential from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
...@@ -31,6 +32,12 @@ function domContentLoaded() { ...@@ -31,6 +32,12 @@ function domContentLoaded() {
service: mediator.service, service: mediator.service,
}, },
}).$mount(confidentialEl); }).$mount(confidentialEl);
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
} }
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker'); new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
......
...@@ -7,7 +7,11 @@ export default class SidebarMediator { ...@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor(options) { constructor(options) {
if (!SidebarMediator.singleton) { if (!SidebarMediator.singleton) {
this.store = new Store(options); this.store = new Store(options);
this.service = new Service(options.endpoint); this.service = new Service({
endpoint: options.endpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
SidebarMediator.singleton = this; SidebarMediator.singleton = this;
} }
...@@ -26,6 +30,10 @@ export default class SidebarMediator { ...@@ -26,6 +30,10 @@ export default class SidebarMediator {
return this.service.update(field, selected.length === 0 ? [0] : selected); return this.service.update(field, selected.length === 0 ? [0] : selected);
} }
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
fetch() { fetch() {
this.service.get() this.service.get()
.then(response => response.json()) .then(response => response.json())
...@@ -35,4 +43,23 @@ export default class SidebarMediator { ...@@ -35,4 +43,23 @@ export default class SidebarMediator {
}) })
.catch(() => new Flash('Error occured when fetching sidebar data')); .catch(() => new Flash('Error occured when fetching sidebar data'));
} }
fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json())
.then((data) => {
this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects;
});
}
moveIssue() {
return this.service.moveIssue(this.store.moveToProjectId)
.then(response => response.json())
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
}
});
}
} }
...@@ -13,6 +13,8 @@ export default class SidebarStore { ...@@ -13,6 +13,8 @@ export default class SidebarStore {
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
}; };
this.autocompleteProjects = [];
this.moveToProjectId = 0;
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
...@@ -53,4 +55,12 @@ export default class SidebarStore { ...@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees() { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
setAutocompleteProjects(projects) {
this.autocompleteProjects = projects;
}
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
} }
...@@ -193,7 +193,7 @@ ...@@ -193,7 +193,7 @@
min-width: 240px; min-width: 240px;
max-width: 500px; max-width: 500px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 2px;
font-size: 14px; font-size: 14px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
padding: 8px 0; padding: 8px 0;
...@@ -618,6 +618,11 @@ ...@@ -618,6 +618,11 @@
border-top: 1px solid $dropdown-divider-color; border-top: 1px solid $dropdown-divider-color;
} }
.dropdown-footer-content {
padding-left: 10px;
padding-right: 10px;
}
.dropdown-due-date-footer { .dropdown-due-date-footer {
padding-top: 0; padding-top: 0;
margin-left: 10px; margin-left: 10px;
...@@ -729,6 +734,7 @@ ...@@ -729,6 +734,7 @@
#{$selector}.dropdown-menu, #{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav { #{$selector}.dropdown-menu-nav {
li { li {
display: block;
padding: 0 1px; padding: 0 1px;
&:hover { &:hover {
......
...@@ -279,14 +279,25 @@ ...@@ -279,14 +279,25 @@
// TODO: change global style // TODO: change global style
.ajax-project-dropdown, .ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:settings:repository:show"] #select2-drop,
body[data-page="projects:new"] #select2-drop, body[data-page="projects:new"] #select2-drop,
body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop, body[data-page="profiles:show"] #select2-drop,
body[data-page="admin:groups:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop { body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop { &.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color; color: $gl-text-color;
} }
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results { .select2-results {
.select2-no-results, .select2-no-results,
.select2-searching, .select2-searching,
......
...@@ -707,6 +707,8 @@ ...@@ -707,6 +707,8 @@
} }
.boards-switcher { .boards-switcher {
@include new-style-dropdown;
padding-right: 10px; padding-right: 10px;
} }
......
...@@ -473,7 +473,7 @@ ...@@ -473,7 +473,7 @@
padding-top: 6px; padding-top: 6px;
} }
.open .dropdown-menu { .dropdown-menu {
width: 100%; width: 100%;
} }
} }
...@@ -486,6 +486,24 @@ ...@@ -486,6 +486,24 @@
} }
} }
.sidebar-move-issue-dropdown {
@include new-style-dropdown;
}
.sidebar-move-issue-confirmation-button {
width: 100%;
&.is-loading {
.sidebar-move-issue-confirmation-loading-icon {
display: inline-block;
}
}
}
.sidebar-move-issue-confirmation-loading-icon {
display: none;
}
.detail-page-description { .detail-page-description {
padding: 16px 0; padding: 16px 0;
......
...@@ -61,6 +61,10 @@ ...@@ -61,6 +61,10 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
} }
.dropdown-menu.dropdown-menu-align-right {
margin-top: -2px;
}
} }
.form-horizontal { .form-horizontal {
...@@ -356,3 +360,7 @@ ...@@ -356,3 +360,7 @@
} }
} }
} }
.member-form-control {
@include new-style-dropdown;
}
...@@ -290,6 +290,7 @@ ...@@ -290,6 +290,7 @@
.dropdown-toggle { .dropdown-toggle {
.fa { .fa {
margin-left: 0;
color: inherit; color: inherit;
} }
} }
...@@ -723,7 +724,14 @@ ...@@ -723,7 +724,14 @@
.approvers-list { .approvers-list {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 5px; }
.approvers-list {
.link-to-member-avatar:not(:first-child) {
img {
margin-left: 0;
}
}
} }
.unapprove-btn { .unapprove-btn {
...@@ -797,3 +805,7 @@ ...@@ -797,3 +805,7 @@
} }
} }
} }
.merge-request-form {
@include new-style-dropdown;
}
...@@ -815,6 +815,8 @@ a.allowed-to-push { ...@@ -815,6 +815,8 @@ a.allowed-to-push {
.new-protected-branch, .new-protected-branch,
.new-protected-tag { .new-protected-tag {
@include new-style-dropdown;
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -841,19 +843,9 @@ a.allowed-to-push { ...@@ -841,19 +843,9 @@ a.allowed-to-push {
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
margin-bottom: 30px; @include new-style-dropdown;
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
&.is-active { margin-bottom: 30px;
font-weight: $gl-font-weight-bold;
}
}
.settings-message { .settings-message {
margin: 0; margin: 0;
......
...@@ -117,11 +117,14 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -117,11 +117,14 @@ class Admin::UsersController < Admin::ApplicationController
user_params_with_pass = user_params.dup user_params_with_pass = user_params.dup
if params[:user][:password].present? if params[:user][:password].present?
user_params_with_pass.merge!( password_params = {
password: params[:user][:password], password: params[:user][:password],
password_confirmation: params[:user][:password_confirmation], password_confirmation: params[:user][:password_confirmation]
password_expires_at: Time.now }
)
password_params[:password_expires_at] = Time.now unless changing_own_password?
user_params_with_pass.merge!(password_params)
end end
respond_to do |format| respond_to do |format|
...@@ -167,6 +170,10 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -167,6 +170,10 @@ class Admin::UsersController < Admin::ApplicationController
protected protected
def changing_own_password?
user == current_user
end
def user def user
@user ||= User.find_by!(username: params[:id]) @user ||= User.find_by!(username: params[:id])
end end
......
...@@ -210,7 +210,7 @@ class ApplicationController < ActionController::Base ...@@ -210,7 +210,7 @@ class ApplicationController < ActionController::Base
end end
def check_password_expiration def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && current_user.allow_password_authentication? if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
return redirect_to new_profile_password_path return redirect_to new_profile_password_path
end end
end end
......
...@@ -45,12 +45,6 @@ class AutocompleteController < ApplicationController ...@@ -45,12 +45,6 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id]) project = Project.find_by_id(params[:project_id])
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
no_project = {
id: 0,
name_with_namespace: 'No project'
}
projects.unshift(no_project) unless params[:offset_id].present?
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end end
......
module RequiresWhitelistedMonitoringClient module RequiresWhitelistedMonitoringClient
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::CurrentSettings
included do included do
before_action :validate_ip_whitelisted_or_valid_token! before_action :validate_ip_whitelisted_or_valid_token!
end end
......
class PasswordsController < Devise::PasswordsController class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
before_action :resource_from_email, only: [:create] before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create] before_action :prevent_ldap_reset, only: [:create]
before_action :throttle_reset, only: [:create] before_action :throttle_reset, only: [:create]
def edit def edit
...@@ -40,11 +38,11 @@ class PasswordsController < Devise::PasswordsController ...@@ -40,11 +38,11 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email) self.resource = resource_class.find_by_email(email)
end end
def check_password_authentication_available def prevent_ldap_reset
return if current_application_settings.password_authentication_enabled? && (resource.nil? || resource.allow_password_authentication?) return unless resource&.ldap_user?
redirect_to after_sending_reset_password_instructions_path_for(resource_name), redirect_to after_sending_reset_password_instructions_path_for(resource_name),
alert: "Password authentication is unavailable." alert: "Cannot reset password for LDAP user."
end end
def throttle_reset def throttle_reset
......
...@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end end
def authorize_change_password! def authorize_change_password!
render_404 unless @user.allow_password_authentication? render_404 if @user.ldap_user?
end end
def user_params def user_params
......
...@@ -94,6 +94,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -94,6 +94,6 @@ class Projects::ApplicationController < ApplicationController
end end
def require_pages_enabled! def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled not_found unless @project.pages_available?
end end
end end
...@@ -17,7 +17,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -17,7 +17,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update] before_action :authorize_update_issue!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -131,25 +131,33 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -131,25 +131,33 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_issue_json
end
end
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
def move
params.require(:move_to_project_id)
if params[:move_to_project_id].to_i > 0 if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id]) new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project) return render_404 unless issue.can_move?(current_user, new_project)
move_service = Issues::MoveService.new(project, current_user) @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
@issue = move_service.execute(@issue, new_project)
end end
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
if @issue.valid? render_issue_json
render json: IssueSerializer.new.represent(@issue)
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
end end
end end
...@@ -260,6 +268,14 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -260,6 +268,14 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user) return render_404 unless @project.feature_available?(:issues, current_user)
end end
def render_issue_json
if @issue.valid?
render json: IssueSerializer.new.represent(@issue)
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
end
def issue_params def issue_params
params.require(:issue).permit(*issue_params_attributes) params.require(:issue).permit(*issue_params_attributes)
end end
......
class Projects::LfsApiController < Projects::GitHttpClientController class Projects::LfsApiController < Projects::GitHttpClientController
include ApplicationSettingsHelper
include ApplicationHelper
include GitlabRoutingHelper
include LfsRequest include LfsRequest
skip_before_action :lfs_check_access!, only: [:deprecated] skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
def batch def batch
unless objects.present? unless objects.present?
...@@ -90,4 +94,16 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -90,4 +94,16 @@ class Projects::LfsApiController < Projects::GitHttpClientController
} }
} }
end end
def lfs_check_batch_operation!
if upload_request? && Gitlab::Geo.secondary?
render(
json: {
message: "You cannot write to a secondary GitLab Geo instance. Please use #{geo_primary_default_url_to_repo(project)} instead."
},
content_type: "application/vnd.git-lfs+json",
status: 403
)
end
end
end end
...@@ -305,7 +305,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -305,7 +305,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :failed return :failed
end end
merge_request_service = MergeRequests::MergeService.new(@project, current_user, merge_params) merge_request_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params)
unless merge_request_service.hooks_validation_pass?(@merge_request) unless merge_request_service.hooks_validation_pass?(@merge_request)
return :hook_validation_error return :hook_validation_error
...@@ -313,7 +313,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -313,7 +313,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
@merge_request.update(merge_error: nil, squash: merge_params[:squash]) @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
if params[:merge_when_pipeline_succeeds].present? if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.head_pipeline return :failed unless @merge_request.head_pipeline
......
...@@ -205,7 +205,7 @@ module ApplicationHelper ...@@ -205,7 +205,7 @@ module ApplicationHelper
end end
def support_url def support_url
current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end end
def page_filter_path(options = {}) def page_filter_path(options = {})
......
...@@ -2,6 +2,8 @@ module ApplicationSettingsHelper ...@@ -2,6 +2,8 @@ module ApplicationSettingsHelper
prepend EE::ApplicationSettingsHelper prepend EE::ApplicationSettingsHelper
extend self extend self
include Gitlab::CurrentSettings
delegate :gravatar_enabled?, delegate :gravatar_enabled?,
:signup_enabled?, :signup_enabled?,
:password_authentication_enabled?, :password_authentication_enabled?,
...@@ -83,6 +85,18 @@ module ApplicationSettingsHelper ...@@ -83,6 +85,18 @@ module ApplicationSettingsHelper
end end
end end
def key_restriction_options_for_select(type)
bit_size_options = Gitlab::SSHPublicKey.supported_sizes(type).map do |bits|
["Must be at least #{bits} bits", bits]
end
[
['Are allowed', 0],
*bit_size_options,
['Are forbidden', ApplicationSetting::FORBIDDEN_KEY_VALUE]
]
end
def repository_storages_options_for_select def repository_storages_options_for_select
options = Gitlab.config.repositories.storages.map do |name, storage| options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name] ["#{name} - #{storage['path']}", name]
...@@ -115,6 +129,9 @@ module ApplicationSettingsHelper ...@@ -115,6 +129,9 @@ module ApplicationSettingsHelper
:domain_blacklist_enabled, :domain_blacklist_enabled,
:domain_blacklist_raw, :domain_blacklist_raw,
:domain_whitelist_raw, :domain_whitelist_raw,
:dsa_key_restriction,
:ecdsa_key_restriction,
:ed25519_key_restriction,
:email_author_in_body, :email_author_in_body,
:enabled_git_access_protocol, :enabled_git_access_protocol,
:gravatar_enabled, :gravatar_enabled,
...@@ -158,6 +175,7 @@ module ApplicationSettingsHelper ...@@ -158,6 +175,7 @@ module ApplicationSettingsHelper
:repository_storages, :repository_storages,
:require_two_factor_authentication, :require_two_factor_authentication,
:restricted_visibility_levels, :restricted_visibility_levels,
:rsa_key_restriction,
:send_user_confirmation_email, :send_user_confirmation_email,
:sentry_dsn, :sentry_dsn,
:sentry_enabled, :sentry_enabled,
......
module AuthHelper module AuthHelper
include Gitlab::CurrentSettings
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
......
...@@ -106,9 +106,11 @@ module DropdownsHelper ...@@ -106,9 +106,11 @@ module DropdownsHelper
end end
end end
def dropdown_footer(&block) def dropdown_footer(add_content_class: false, &block)
content_tag(:div, class: "dropdown-footer") do content_tag(:div, class: "dropdown-footer") do
if block if add_content_class
content_tag(:div, capture(&block), class: "dropdown-footer-content")
else
capture(&block) capture(&block)
end end
end end
......
module FormHelper module FormHelper
prepend ::EE::FormHelper prepend ::EE::FormHelper
def form_errors(model) def form_errors(model, type: 'form')
return unless model.errors.any? return unless model.errors.any?
pluralized = 'error'.pluralize(model.errors.count) pluralized = 'error'.pluralize(model.errors.count)
headline = "The form contains the following #{pluralized}:" headline = "The #{type} contains the following #{pluralized}:"
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) << content_tag(:h4, headline) <<
......
...@@ -215,12 +215,10 @@ module IssuablesHelper ...@@ -215,12 +215,10 @@ module IssuablesHelper
endpoint: project_issue_path(@project, issuable), endpoint: project_issue_path(@project, issuable),
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable),
canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
isConfidential: issuable.confidential, isConfidential: issuable.confidential,
markdownPreviewUrl: preview_markdown_path(@project), markdownPreviewUrl: preview_markdown_path(@project),
markdownDocs: help_page_path('user/markdown'), markdownDocs: help_page_path('user/markdown'),
projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path, projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path, projectNamespace: ref_project.namespace.full_path,
...@@ -369,6 +367,8 @@ module IssuablesHelper ...@@ -369,6 +367,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable) def issuable_sidebar_options(issuable, can_edit_issuable)
{ {
endpoint: "#{issuable_json_path(issuable)}?basic=true", endpoint: "#{issuable_json_path(issuable)}?basic=true",
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable, editable: can_edit_issuable,
currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url), currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
rootPath: root_path, rootPath: root_path,
......
...@@ -77,10 +77,9 @@ module LicenseHelper ...@@ -77,10 +77,9 @@ module LicenseHelper
def show_promotions?(selected_user = current_user) def show_promotions?(selected_user = current_user)
return false unless selected_user return false unless selected_user
return @show_promotions if defined?(@show_promotions)
@show_promotions = if Gitlab::CurrentSettings.current_application_settings
if current_application_settings.should_check_namespace_plan? .should_check_namespace_plan?
true true
else else
license = License.current license = License.current
......
module ProjectsHelper module ProjectsHelper
include Gitlab::CurrentSettings
def link_to_project(project) def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name') title = content_tag(:span, project.name, class: 'project-name')
...@@ -70,12 +72,6 @@ module ProjectsHelper ...@@ -70,12 +72,6 @@ module ProjectsHelper
output.html_safe output.html_safe
end end
if current_user
project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down")
end
end
"#{namespace_link} / #{project_link}".html_safe "#{namespace_link} / #{project_link}".html_safe
end end
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
include Gitlab::CurrentSettings
around_action :render_with_default_locale around_action :render_with_default_locale
helper ApplicationHelper helper ApplicationHelper
helper MarkupHelper helper MarkupHelper
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?, :current_application_settings
default from: proc { default_sender_address.format } default from: proc { default_sender_address.format }
default reply_to: proc { default_reply_to_address.format } default reply_to: proc { default_reply_to_address.format }
......
...@@ -14,6 +14,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -14,6 +14,11 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters [\r\n] # any number of newline characters
}x }x
# Setting a key restriction to `-1` means that all keys of this type are
# forbidden.
FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN
SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
...@@ -159,6 +164,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -159,6 +164,12 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
validates :allowed_key_types, presence: true
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level| value&.each do |level|
unless Gitlab::VisibilityLevel.options.value?(level) unless Gitlab::VisibilityLevel.options.value?(level)
...@@ -184,6 +195,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -184,6 +195,7 @@ class ApplicationSetting < ActiveRecord::Base
end end
before_validation :ensure_uuid! before_validation :ensure_uuid!
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
...@@ -234,6 +246,9 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -234,6 +246,9 @@ class ApplicationSetting < ActiveRecord::Base
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil, help_page_text: nil,
help_page_hide_commercial_content: false, help_page_hide_commercial_content: false,
...@@ -252,6 +267,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -252,6 +267,7 @@ class ApplicationSetting < ActiveRecord::Base
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
password_authentication_enabled: Settings.gitlab['password_authentication_enabled'], password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
performance_bar_allowed_group_id: nil, performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0,
plantuml_enabled: false, plantuml_enabled: false,
plantuml_url: nil, plantuml_url: nil,
project_export_enabled: true, project_export_enabled: true,
...@@ -460,6 +476,18 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -460,6 +476,18 @@ class ApplicationSetting < ActiveRecord::Base
usage_ping_can_be_configured? && super usage_ping_can_be_configured? && super
end end
def allowed_key_types
SUPPORTED_KEY_TYPES.select do |type|
key_restriction_for(type) != FORBIDDEN_KEY_VALUE
end
end
def key_restriction_for(type)
attr_name = "#{type}_key_restriction"
has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
end
private private
def ensure_uuid! def ensure_uuid!
......
...@@ -3,6 +3,7 @@ module Ci ...@@ -3,6 +3,7 @@ module Ci
include TokenAuthenticatable include TokenAuthenticatable
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Importable
prepend EE::Build prepend EE::Build
belongs_to :runner belongs_to :runner
...@@ -29,6 +30,7 @@ module Ci ...@@ -29,6 +30,7 @@ module Ci
validates :coverage, numericality: true, allow_blank: true validates :coverage, numericality: true, allow_blank: true
validates :ref, presence: true validates :ref, presence: true
validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
...@@ -38,6 +40,7 @@ module Ci ...@@ -38,6 +40,7 @@ module Ci
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :codequality, ->() { where(name: %w[codequality codeclimate]) } scope :codequality, ->() { where(name: %w[codequality codeclimate]) }
scope :ref_protected, -> { where(protected: true) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
......
...@@ -48,6 +48,7 @@ module Ci ...@@ -48,6 +48,7 @@ module Ci
validates :sha, presence: { unless: :importing? } validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? }
validates :status, presence: { unless: :importing? } validates :status, presence: { unless: :importing? }
validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
......
...@@ -6,7 +6,7 @@ module Ci ...@@ -6,7 +6,7 @@ module Ci
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour ONLINE_CONTACT_TIMEOUT = 1.hour
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
has_many :builds has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
...@@ -36,11 +36,17 @@ module Ci ...@@ -36,11 +36,17 @@ module Ci
end end
validate :tag_constraints validate :tag_constraints
validates :access_level, presence: true
acts_as_taggable acts_as_taggable
after_destroy :cleanup_runner_queue after_destroy :cleanup_runner_queue
enum access_level: {
not_protected: 0,
ref_protected: 1
}
# Searches for runners matching the given query. # Searches for runners matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
...@@ -107,6 +113,8 @@ module Ci ...@@ -107,6 +113,8 @@ module Ci
end end
def can_pick?(build) def can_pick?(build)
return false if self.ref_protected? && !build.protected?
assignable_for?(build.project) && accepting_tags?(build) assignable_for?(build.project) && accepting_tags?(build)
end end
......
...@@ -251,6 +251,28 @@ class Commit ...@@ -251,6 +251,28 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true) project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end end
def cherry_pick_description(user)
message_body = "(cherry picked from commit #{sha})"
if merged_merge_request?(user)
commits_in_merge_request = merged_merge_request(user).commits
if commits_in_merge_request.present?
message_body << "\n"
commits_in_merge_request.reverse.each do |commit_in_merge|
message_body << "\n#{commit_in_merge.short_id} #{commit_in_merge.title}"
end
end
end
message_body
end
def cherry_pick_message(user)
%Q{#{message}\n\n#{cherry_pick_description(user)}}
end
def revert_description(user) def revert_description(user)
if merged_merge_request?(user) if merged_merge_request?(user)
"This reverts merge request #{merged_merge_request(user).to_reference}" "This reverts merge request #{merged_merge_request(user).to_reference}"
......
module Elastic module Elastic
module ApplicationSearch module ApplicationSearch
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend Gitlab::CurrentSettings
included do included do
include Elasticsearch::Model include Elasticsearch::Model
include Gitlab::CurrentSettings
index_name [Rails.application.class.parent_name.downcase, Rails.env].join('-') index_name [Rails.application.class.parent_name.downcase, Rails.env].join('-')
......
...@@ -28,7 +28,7 @@ module Spammable ...@@ -28,7 +28,7 @@ module Spammable
def submittable_as_spam? def submittable_as_spam?
if user_agent_detail if user_agent_detail
user_agent_detail.submittable? && current_application_settings.akismet_enabled user_agent_detail.submittable? && Gitlab::CurrentSettings.current_application_settings.akismet_enabled
else else
false false
end end
......
...@@ -303,7 +303,13 @@ class Issue < ActiveRecord::Base ...@@ -303,7 +303,13 @@ class Issue < ActiveRecord::Base
end end
end end
def update_project_counter_caches?
state_changed? || confidential_changed?
end
def update_project_counter_caches def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenIssuesCountService.new(project).refresh_cache Projects::OpenIssuesCountService.new(project).refresh_cache
end end
......
class IssueAssignee < ActiveRecord::Base class IssueAssignee < ActiveRecord::Base
extend Gitlab::CurrentSettings
belongs_to :issue belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id belongs_to :assignee, class_name: "User", foreign_key: :user_id
...@@ -9,7 +7,7 @@ class IssueAssignee < ActiveRecord::Base ...@@ -9,7 +7,7 @@ class IssueAssignee < ActiveRecord::Base
# EE-specific # EE-specific
def update_elasticsearch_index def update_elasticsearch_index
if current_application_settings.elasticsearch_indexing? if Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing?
ElasticIndexerWorker.perform_async( ElasticIndexerWorker.perform_async(
:update, :update,
'Issue', 'Issue',
......
require 'digest/md5' require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include Gitlab::CurrentSettings
include Sortable include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i LAST_USED_AT_REFRESH_TIME = 1.day.to_i
...@@ -12,14 +13,19 @@ class Key < ActiveRecord::Base ...@@ -12,14 +13,19 @@ class Key < ActiveRecord::Base
validates :title, validates :title,
presence: true, presence: true,
length: { maximum: 255 } length: { maximum: 255 }
validates :key, validates :key,
presence: true, presence: true,
length: { maximum: 5000 }, length: { maximum: 5000 },
format: { with: /\A(ssh|ecdsa)-.*\Z/ } format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :fingerprint, validates :fingerprint,
uniqueness: true, uniqueness: true,
presence: { message: 'cannot be generated' } presence: { message: 'cannot be generated' }
validate :key_meets_restrictions
# EE-only
scope :ldap, -> { where(type: 'LDAPKey') } scope :ldap, -> { where(type: 'LDAPKey') }
delegate :name, :email, to: :user, prefix: true delegate :name, :email, to: :user, prefix: true
...@@ -82,6 +88,10 @@ class Key < ActiveRecord::Base ...@@ -82,6 +88,10 @@ class Key < ActiveRecord::Base
SystemHooksService.new.execute_hooks_for(self, :destroy) SystemHooksService.new.execute_hooks_for(self, :destroy)
end end
def public_key
@public_key ||= Gitlab::SSHPublicKey.new(key)
end
private private
def generate_fingerprint def generate_fingerprint
...@@ -89,7 +99,27 @@ class Key < ActiveRecord::Base ...@@ -89,7 +99,27 @@ class Key < ActiveRecord::Base
return unless self.key.present? return unless self.key.present?
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint self.fingerprint = public_key.fingerprint
end
def key_meets_restrictions
restriction = current_application_settings.key_restriction_for(public_key.type)
if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE
errors.add(:key, forbidden_key_type_message)
elsif public_key.bits < restriction
errors.add(:key, "must be at least #{restriction} bits")
end
end
def forbidden_key_type_message
allowed_types =
current_application_settings
.allowed_key_types
.map(&:upcase)
.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
"type is forbidden. Must be #{allowed_types}"
end end
def notify_user def notify_user
......
...@@ -130,12 +130,15 @@ class License < ActiveRecord::Base ...@@ -130,12 +130,15 @@ class License < ActiveRecord::Base
# Early adopters should not earn new features as they're # Early adopters should not earn new features as they're
# introduced. # introduced.
EARLY_ADOPTER_FEATURES = [ EARLY_ADOPTER_FEATURES = [
{ ADMIN_AUDIT_LOG_FEATURE => 1 },
{ AUDIT_EVENTS_FEATURE => 1 }, { AUDIT_EVENTS_FEATURE => 1 },
{ AUDITOR_USER_FEATURE => 1 }, { AUDITOR_USER_FEATURE => 1 },
{ BURNDOWN_CHARTS_FEATURE => 1 }, { BURNDOWN_CHARTS_FEATURE => 1 },
{ CONTRIBUTION_ANALYTICS_FEATURE => 1 }, { CONTRIBUTION_ANALYTICS_FEATURE => 1 },
{ CROSS_PROJECT_PIPELINES_FEATURE => 1 }, { CROSS_PROJECT_PIPELINES_FEATURE => 1 },
{ DB_LOAD_BALANCING_FEATURE => 1 },
{ DEPLOY_BOARD_FEATURE => 1 }, { DEPLOY_BOARD_FEATURE => 1 },
{ ELASTIC_SEARCH_FEATURE => 1 },
{ EXPORT_ISSUES_FEATURE => 1 }, { EXPORT_ISSUES_FEATURE => 1 },
{ FAST_FORWARD_MERGE_FEATURE => 1 }, { FAST_FORWARD_MERGE_FEATURE => 1 },
{ FILE_LOCKS_FEATURE => 1 }, { FILE_LOCKS_FEATURE => 1 },
...@@ -146,6 +149,7 @@ class License < ActiveRecord::Base ...@@ -146,6 +149,7 @@ class License < ActiveRecord::Base
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 }, { ISSUE_BOARD_MILESTONE_FEATURE => 1 },
{ ISSUE_WEIGHTS_FEATURE => 1 }, { ISSUE_WEIGHTS_FEATURE => 1 },
{ JENKINS_INTEGRATION_FEATURE => 1 }, { JENKINS_INTEGRATION_FEATURE => 1 },
{ LDAP_EXTRAS_FEATURE => 1 },
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, { MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 }, { MERGE_REQUEST_SQUASH_FEATURE => 1 },
...@@ -154,8 +158,11 @@ class License < ActiveRecord::Base ...@@ -154,8 +158,11 @@ class License < ActiveRecord::Base
{ OBJECT_STORAGE_FEATURE => 1 }, { OBJECT_STORAGE_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 }, { PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 }, { REPOSITORY_MIRRORS_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 } { REPOSITORY_SIZE_LIMIT_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 },
{ VARIABLE_ENVIRONMENT_SCOPE_FEATURE => 1 }
].freeze ].freeze
FEATURES_BY_PLAN = { FEATURES_BY_PLAN = {
......
...@@ -630,6 +630,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -630,6 +630,8 @@ class MergeRequest < ActiveRecord::Base
self.merge_requests_closing_issues.delete_all self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue| closes_issues(current_user).each do |issue|
next if issue.is_a?(ExternalIssue)
self.merge_requests_closing_issues.create!(issue: issue) self.merge_requests_closing_issues.create!(issue: issue)
end end
end end
...@@ -971,7 +973,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -971,7 +973,13 @@ class MergeRequest < ActiveRecord::Base
@base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha) @base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha)
end end
def update_project_counter_caches?
state_changed?
end
def update_project_counter_caches def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end end
......
...@@ -200,6 +200,10 @@ class Namespace < ActiveRecord::Base ...@@ -200,6 +200,10 @@ class Namespace < ActiveRecord::Base
parent.present? parent.present?
end end
def subgroup?
has_parent?
end
def soft_delete_without_removing_associations def soft_delete_without_removing_associations
# We can't use paranoia's `#destroy` since this will hard-delete projects. # We can't use paranoia's `#destroy` since this will hard-delete projects.
# Project uses `pending_delete` instead of the acts_as_paranoia gem. # Project uses `pending_delete` instead of the acts_as_paranoia gem.
......
...@@ -22,6 +22,7 @@ class Project < ActiveRecord::Base ...@@ -22,6 +22,7 @@ class Project < ActiveRecord::Base
prepend EE::Project prepend EE::Project
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
BoardLimitExceeded = Class.new(StandardError) BoardLimitExceeded = Class.new(StandardError)
...@@ -1231,6 +1232,10 @@ class Project < ActiveRecord::Base ...@@ -1231,6 +1232,10 @@ class Project < ActiveRecord::Base
File.join(pages_path, 'public') File.join(pages_path, 'public')
end end
def pages_available?
Gitlab.config.pages.enabled && !namespace.subgroup?
end
def remove_private_deploy_keys def remove_private_deploy_keys
exclude_keys_linked_to_other_projects = <<-SQL exclude_keys_linked_to_other_projects = <<-SQL
NOT EXISTS ( NOT EXISTS (
......
...@@ -51,7 +51,7 @@ class ProjectFeature < ActiveRecord::Base ...@@ -51,7 +51,7 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
after_commit on: :update do after_commit on: :update do
if current_application_settings.elasticsearch_indexing? if Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing?
ElasticIndexerWorker.perform_async(:update, 'Project', project_id) ElasticIndexerWorker.perform_async(:update, 'Project', project_id)
end end
end end
......
...@@ -3,6 +3,8 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -3,6 +3,8 @@ class ProtectedBranch < ActiveRecord::Base
include ProtectedRef include ProtectedRef
prepend EE::ProtectedRef prepend EE::ProtectedRef
extend Gitlab::CurrentSettings
protected_ref_access_levels :merge, :push protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system # Check if branch name is marked as protected in the system
......
...@@ -934,7 +934,7 @@ class Repository ...@@ -934,7 +934,7 @@ class Repository
committer = user_to_committer(user) committer = user_to_committer(user)
create_commit(message: commit.message, create_commit(message: commit.cherry_pick_message(user),
author: { author: {
email: commit.author_email, email: commit.author_email,
name: commit.author_name, name: commit.author_name,
......
...@@ -11,6 +11,8 @@ class Snippet < ActiveRecord::Base ...@@ -11,6 +11,8 @@ class Snippet < ActiveRecord::Base
include Spammable include Spammable
include Editable include Editable
extend Gitlab::CurrentSettings
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description cache_markdown_field :description
cache_markdown_field :content cache_markdown_field :content
......
...@@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord' ...@@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord'
class User < ActiveRecord::Base class User < ActiveRecord::Base
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
...@@ -621,7 +622,7 @@ class User < ActiveRecord::Base ...@@ -621,7 +622,7 @@ class User < ActiveRecord::Base
end end
def require_personal_access_token_creation_for_git_auth? def require_personal_access_token_creation_for_git_auth?
return false if allow_password_authentication? || ldap_user? return false if current_application_settings.password_authentication_enabled? || ldap_user?
PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none? PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end end
......
...@@ -84,7 +84,7 @@ class WikiPage ...@@ -84,7 +84,7 @@ class WikiPage
# The formatted title of this page. # The formatted title of this page.
def title def title
if @attributes[:title] if @attributes[:title]
self.class.unhyphenize(@attributes[:title]) CGI.unescape_html(self.class.unhyphenize(@attributes[:title]))
else else
"" ""
end end
......
require_dependency 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
include Gitlab::CurrentSettings
desc "User is an instance admin" desc "User is an instance admin"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:admin) { @user&.admin? } condition(:admin) { @user&.admin? }
...@@ -15,7 +13,7 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -15,7 +13,7 @@ class BasePolicy < DeclarativePolicy::Base
desc "The application is restricted from public visibility" desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do condition(:restricted_public_level, scope: :global) do
current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end end
# EE Extensions # EE Extensions
......
class AkismetService class AkismetService
include Gitlab::CurrentSettings
attr_accessor :owner, :text, :options attr_accessor :owner, :text, :options
def initialize(owner, text, options = {}) def initialize(owner, text, options = {})
......
module Auth module Auth
class ContainerRegistryAuthenticationService < BaseService class ContainerRegistryAuthenticationService < BaseService
include Gitlab::CurrentSettings extend Gitlab::CurrentSettings
AUDIENCE = 'container_registry'.freeze AUDIENCE = 'container_registry'.freeze
......
...@@ -12,7 +12,8 @@ module Ci ...@@ -12,7 +12,8 @@ module Ci
tag: tag?, tag: tag?,
trigger_requests: Array(trigger_request), trigger_requests: Array(trigger_request),
user: current_user, user: current_user,
pipeline_schedule: schedule pipeline_schedule: schedule,
protected: project.protected_for?(ref)
) )
result = validate(current_user, result = validate(current_user,
......
...@@ -78,7 +78,9 @@ module Ci ...@@ -78,7 +78,9 @@ module Ci
end end
def new_builds def new_builds
Ci::Build.pending.unstarted builds = Ci::Build.pending.unstarted
builds = builds.ref_protected if runner.ref_protected?
builds
end end
def shared_runner_build_limits_feature_enabled? def shared_runner_build_limits_feature_enabled?
......
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
CLONE_ACCESSORS = %i[pipeline project ref tag options commands name CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
allow_failure stage_id stage stage_idx trigger_request allow_failure stage_id stage stage_idx trigger_request
yaml_variables when environment coverage_regex yaml_variables when environment coverage_regex
description tag_list].freeze description tag_list protected].freeze
def execute(build) def execute(build)
reprocess!(build).tap do |new_build| reprocess!(build).tap do |new_build|
......
...@@ -58,6 +58,7 @@ class IssuableBaseService < BaseService ...@@ -58,6 +58,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id) params.delete(:assignee_id)
params.delete(:due_date) params.delete(:due_date)
params.delete(:canonical_issue_id) params.delete(:canonical_issue_id)
params.delete(:project)
end end
filter_assignee(issuable) filter_assignee(issuable)
......
...@@ -6,7 +6,7 @@ module Issues ...@@ -6,7 +6,7 @@ module Issues
handle_move_between_iids(issue) handle_move_between_iids(issue)
filter_spam_check_params filter_spam_check_params
change_issue_duplicate(issue) change_issue_duplicate(issue)
update(issue) move_issue_to_new_project(issue) || update(issue)
end end
def before_update(issue) def before_update(issue)
...@@ -74,6 +74,17 @@ module Issues ...@@ -74,6 +74,17 @@ module Issues
end end
end end
def move_issue_to_new_project(issue)
target_project = params.delete(:target_project)
return unless target_project &&
issue.can_move?(current_user, target_project) &&
target_project != issue.project
update(issue)
Issues::MoveService.new(project, current_user).execute(issue, target_project)
end
private private
def get_issue_if_allowed(project, iid) def get_issue_if_allowed(project, iid)
......
module Projects module Projects
class AfterImportService class AfterImportService
RESERVED_REFS_REGEXP = RESERVED_REF_PREFIXES = Repository::RESERVED_REFS_NAMES.map { |n| File.join('refs', n, '/') }
%r{\Arefs/(?:#{Regexp.union(*Repository::RESERVED_REFS_NAMES)})/}
def initialize(project) def initialize(project)
@project = project @project = project
...@@ -9,7 +8,7 @@ module Projects ...@@ -9,7 +8,7 @@ module Projects
def execute def execute
Projects::HousekeepingService.new(@project).execute do Projects::HousekeepingService.new(@project).execute do
repository.delete_refs(*garbage_refs) repository.delete_all_refs_except(RESERVED_REF_PREFIXES)
end end
rescue Projects::HousekeepingService::LeaseTaken => e rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info( Rails.logger.info(
...@@ -18,10 +17,6 @@ module Projects ...@@ -18,10 +17,6 @@ module Projects
private private
def garbage_refs
@garbage_refs ||= repository.all_ref_names_except(RESERVED_REFS_REGEXP)
end
def repository def repository
@repository ||= @project.repository @repository ||= @project.repository
end end
......
module Projects module Projects
class UpdatePagesService < BaseService class UpdatePagesService < BaseService
include Gitlab::CurrentSettings
BLOCK_SIZE = 32.kilobytes BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze SITE_PATH = 'public/'.freeze
......
...@@ -506,6 +506,24 @@ module QuickActions ...@@ -506,6 +506,24 @@ module QuickActions
end end
end end
desc 'Move this issue to another project.'
explanation do |path_to_project|
"Moves this issue to #{path_to_project}."
end
params 'path/to/project'
condition do
issuable.is_a?(Issue) &&
issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :move do |target_project_path|
target_project = Project.find_by_full_path(target_project_path)
if target_project.present?
@updates[:target_project] = target_project
end
end
def extract_users(params) def extract_users(params)
return [] if params.nil? return [] if params.nil?
......
...@@ -36,7 +36,8 @@ module SlashCommands ...@@ -36,7 +36,8 @@ module SlashCommands
def valid_token? def valid_token?
ActiveSupport::SecurityUtils.variable_size_secure_compare( ActiveSupport::SecurityUtils.variable_size_secure_compare(
current_application_settings.slack_app_verification_token, Gitlab::CurrentSettings.current_application_settings
.slack_app_verification_token,
params[:token] params[:token]
) )
end end
......
class UploadService class UploadService
include Gitlab::CurrentSettings
def initialize(model, file, uploader_class = FileUploader) def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class @model, @file, @uploader_class = model, file, uploader_class
end end
......
module Users module Users
class BuildService < BaseService class BuildService < BaseService
prepend ::EE::Users::BuildService prepend ::EE::Users::BuildService
include Gitlab::CurrentSettings
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
......
...@@ -29,9 +29,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator ...@@ -29,9 +29,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator
private private
def valid_url?(value) def valid_url?(value)
return false unless value valid_uri?(value) && valid_protocol?(value)
valid_protocol?(value) && valid_uri?(value)
end end
def valid_uri?(value) def valid_uri?(value)
......
class KeyRestrictionValidator < ActiveModel::EachValidator
FORBIDDEN = -1
def self.supported_sizes(type)
Gitlab::SSHPublicKey.supported_sizes(type)
end
def self.supported_key_restrictions(type)
[0, *supported_sizes(type), FORBIDDEN]
end
def validate_each(record, attribute, value)
unless valid_restriction?(value)
record.errors.add(attribute, "must be forbidden, allowed, or one of these sizes: #{supported_sizes_message}")
end
end
private
def supported_sizes_message
sizes = self.class.supported_sizes(options[:type])
sizes.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
end
def valid_restriction?(value)
choices = self.class.supported_key_restrictions(options[:type])
choices.include?(value)
end
end
...@@ -42,12 +42,7 @@ ...@@ -42,12 +42,7 @@
= link_to "(?)", help_page_path("integration/bitbucket") = link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab") = link_to "(?)", help_page_path("integration/gitlab")
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
...@@ -55,6 +50,7 @@ ...@@ -55,6 +50,7 @@
= f.check_box :project_export_enabled = f.check_box :project_export_enabled
Project export enabled Project export enabled
-# EE-only
- if ldap_enabled? - if ldap_enabled?
.form-group .form-group
= f.label :allow_group_owners_to_manage_ldap, 'LDAP settings', class: 'control-label col-sm-2' = f.label :allow_group_owners_to_manage_ldap, 'LDAP settings', class: 'control-label col-sm-2'
...@@ -67,6 +63,21 @@ ...@@ -67,6 +63,21 @@
If checked, group owners can manage LDAP group links and LDAP member overrides If checked, group owners can manage LDAP group links and LDAP member overrides
= link_to icon('question-circle'), help_page_path('administration/auth/ldap-ee') = link_to icon('question-circle'), help_page_path('administration/auth/ldap-ee')
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- field_name = :"#{type}_key_restriction"
.form-group
= f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
.col-sm-10
= f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
%fieldset %fieldset
%legend Account and Limit Settings %legend Account and Limit Settings
.form-group .form-group
...@@ -180,7 +191,7 @@ ...@@ -180,7 +191,7 @@
.checkbox .checkbox
= f.label :password_authentication_enabled do = f.label :password_authentication_enabled do
= f.check_box :password_authentication_enabled = f.check_box :password_authentication_enabled
Password authentication enabled Sign-in enabled
- if omniauth_enabled? && button_based_providers.any? - if omniauth_enabled? && button_based_providers.any?
.form-group .form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
......
...@@ -20,8 +20,10 @@ ...@@ -20,8 +20,10 @@
Account Account
- if current_application_settings.should_check_namespace_plan? - if current_application_settings.should_check_namespace_plan?
= nav_link(controller: :billings) do = nav_link(controller: :billings) do
= link_to profile_billings_path, title: 'Billing' do = sidebar_link profile_billings_path, title: _('Billing') do
%span .nav-icon-container
= custom_icon('credit_card')
%span.nav-item-name
Billing Billing
- if current_application_settings.user_oauth_applications? - if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do = nav_link(controller: 'oauth/applications') do
......
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span %span
CI / CD CI / CD
- if Gitlab.config.pages.enabled - if @project.pages_available?
= nav_link(controller: :pages) do = nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do = link_to project_pages_path(@project), title: 'Pages' do
%span %span
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
= link_to profile_emails_path, title: 'Emails' do = link_to profile_emails_path, title: 'Emails' do
%span %span
Emails Emails
- if current_user.allow_password_authentication? - unless current_user.ldap_user?
= nav_link(controller: :passwords) do = nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do = link_to edit_profile_password_path, title: 'Password' do
%span %span
......
...@@ -15,12 +15,4 @@ ...@@ -15,12 +15,4 @@
window.uploads_path = "#{project_uploads_path(project)}"; window.uploads_path = "#{project_uploads_path(project)}";
window.preview_markdown_path = "#{preview_markdown_path(project)}"; window.preview_markdown_path = "#{preview_markdown_path(project)}";
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
= dropdown_title("Go to a project")
= dropdown_filter("Search your projects")
= dropdown_content
= dropdown_loading
= render template: "layouts/application" = render template: "layouts/application"
%li.key-list-item %li.key-list-item
.pull-left.append-right-10 .pull-left.append-right-10
= icon 'key', class: "settings-list-icon hidden-xs" - if key.valid?
= icon 'key', class: 'settings-list-icon hidden-xs'
- else
= icon 'exclamation-triangle', class: 'settings-list-icon hidden-xs has-tooltip',
title: key.errors.full_messages.join(', ')
.key-list-item-info .key-list-item-info
= link_to path_to_key(key, is_admin), class: "title" do = link_to path_to_key(key, is_admin), class: "title" do
= key.title = key.title
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
%strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8 .col-md-8
= form_errors(@key, type: 'key') unless @key.valid?
%p %p
%span.light Fingerprint: %span.light Fingerprint:
%code.key-fingerprint= @key.fingerprint %code.key-fingerprint= @key.fingerprint
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Due date Due date
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value .value
.value-content .value-content
%span.no-value{ "v-if" => "!issue.dueDate" } %span.no-value{ "v-if" => "!issue.dueDate" }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Labels Labels
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value.issuable-show-labels .value.issuable-show-labels
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None None
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Milestone Milestone
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading") = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right" = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value .value
%span.no-value{ "v-if" => "!issue.milestone" } %span.no-value{ "v-if" => "!issue.milestone" }
None None
......
...@@ -6,6 +6,12 @@ ...@@ -6,6 +6,12 @@
.checkbox .checkbox
= f.check_box :active = f.check_box :active
%span.light Paused Runners don't accept new jobs %span.light Paused Runners don't accept new jobs
.form-group
= label :protected, "Protected", class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :access_level, {}, 'ref_protected', 'not_protected'
%span.light This runner will only run on pipelines trigged on protected branches
.form-group .form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label' = label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
%tr %tr
%td Active %td Active
%td= @runner.active? ? 'Yes' : 'No' %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
%td= @runner.ref_protected? ? 'Yes' : 'No'
%tr %tr
%td Can run untagged jobs %td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No' %td= @runner.run_untagged? ? 'Yes' : 'No'
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do
%span %span
Pipelines Pipelines
- if Gitlab.config.pages.enabled - if @project.pages_available?
= nav_link(controller: :pages) do = nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do = link_to project_pages_path(@project), title: 'Pages' do
%span %span
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></svg>
...@@ -29,18 +29,6 @@ ...@@ -29,18 +29,6 @@
= render 'shared/issuable/form/metadata', issuable: issuable, form: form = render 'shared/issuable/form/metadata', issuable: issuable, form: form
- if issuable.can_move?(current_user)
%hr
.form-group
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
= hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
= render 'shared/issuable/approvals', issuable: issuable, form: form = render 'shared/issuable/approvals', issuable: issuable, form: form
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
Milestone Milestone
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
- if issuable.milestone - if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 } = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
Due date Due date
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
%span.value-content %span.value-content
- if issuable.due_date - if issuable.due_date
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
Labels Labels
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable - if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any? - if selected_labels.any?
- selected_labels.each do |label| - selected_labels.each do |label|
...@@ -168,5 +168,22 @@ ...@@ -168,5 +168,22 @@
%cite{ title: project_ref } %cite{ title: project_ref }
= project_ref = project_ref
= clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left") = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
- if current_user && issuable.can_move?(current_user)
.block.js-sidebar-move-issue-block
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: 'Move issue' }
= custom_icon('icon_arrow_right')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
data: { toggle: 'dropdown' } }
Move issue
.dropdown-menu.dropdown-menu-selectable
= dropdown_title('Move issue')
= dropdown_filter('Search project', search_id: 'sidebar-move-issue-dropdown-search')
= dropdown_content
= dropdown_loading
= dropdown_footer add_content_class: true do
%button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
Move
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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