Commit 817d454b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee' into 'master'

CE upstream

See merge request !1201
parents f93ee986 44af80e2
...@@ -430,7 +430,7 @@ merge request: ...@@ -430,7 +430,7 @@ merge request:
1. [Newlines styleguide][newlines-styleguide] 1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md) 1. [Testing](doc/development/testing.md)
1. [JavaScript (ES6)](https://github.com/airbnb/javascript) 1. [JavaScript (ES6)](https://github.com/airbnb/javascript)
1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5) 1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5)
1. [SCSS styleguide][scss-styleguide] 1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab 1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security contributors to enhance security
......
...@@ -59,20 +59,38 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -59,20 +59,38 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature Freeze ## Feature Freeze
On the 7th of each month, the stable branches for the upcoming release will On the 7th of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
be frozen for major changes. Merge requests may still be merged into master Merge requests may still be merged into master during this period,
during this period. By freezing the stable branches prior to a release there's but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
no need to worry about last minute merge requests potentially breaking a lot of By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
things.
What is considered to be a major change is determined on a case by case basis as Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
this definition depends very much on the context of changes. For example, a 5 and security issues will be cherry-picked into the stable branch.
line change might have a big impact on the entire application. Ultimately the Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
decision will be made by the maintainers and the release managers. These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd).
If you think a merge request should go into the upcoming release even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
1. a Release Manager
2. an Engineering Lead
3. an Engineering Director, the VP of Engineering, or the CTO
You can find who is who on the [team page](https://about.gitlab.com/team/).
Whether an exception is made is determined by weighing the benefit and urgency of the change
(how important it is to the company that this is released _right now_ instead of in a month)
against the potential negative impact
(things breaking without enough time to comfortably find and fix them before the release on the 22nd).
When in doubt, we err on the side of _not_ cherry-picking.
For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
During the feature freeze all merge requests that are meant to go into the upcoming During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set. Merge requests without a milestone and this label will ~"Pick into Stable" set, so that release managers can find and pick them.
Merge requests without a milestone and this label will
not be merged into any stable branches. not be merged into any stable branches.
## Copy & paste responses ## Copy & paste responses
......
...@@ -10,7 +10,6 @@ function requireAll(context) { return context.keys().map(context); } ...@@ -10,7 +10,6 @@ function requireAll(context) { return context.keys().map(context); }
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete'); require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/datepicker');
require('jquery-ui/ui/draggable'); require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight'); require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable'); require('jquery-ui/ui/sortable');
...@@ -36,8 +35,10 @@ require('bootstrap/js/transition'); ...@@ -36,8 +35,10 @@ require('bootstrap/js/transition');
require('bootstrap/js/tooltip'); require('bootstrap/js/tooltip');
require('bootstrap/js/popover'); require('bootstrap/js/popover');
require('select2/select2.js'); require('select2/select2.js');
window.Pikaday = require('pikaday');
window._ = require('underscore'); window._ = require('underscore');
window.Dropzone = require('dropzone'); window.Dropzone = require('dropzone');
window.Sortable = require('vendor/Sortable');
require('mousetrap'); require('mousetrap');
require('mousetrap/plugins/pause/mousetrap-pause'); require('mousetrap/plugins/pause/mousetrap-pause');
require('./shortcuts'); require('./shortcuts');
......
...@@ -6,7 +6,6 @@ function requireAll(context) { return context.keys().map(context); } ...@@ -6,7 +6,6 @@ function requireAll(context) { return context.keys().map(context); }
window.Vue = require('vue'); window.Vue = require('vue');
window.Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
window.Sortable = require('vendor/Sortable');
requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
......
/* global Vue */ /* global Vue */
/* global dateFormat */
Vue.filter('due-date', (value) => { Vue.filter('due-date', (value) => {
const date = new Date(value); const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date); return dateFormat(date, 'mmm d, yyyy');
}); });
...@@ -99,6 +99,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -99,6 +99,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
case 'projects:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form')); new gl.GLForm($('.milestone-form'));
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ /* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */
/* global Pikaday */
(function(global) { (function(global) {
class DueDateSelect { class DueDateSelect {
...@@ -25,11 +27,14 @@ ...@@ -25,11 +27,14 @@
this.initGlDropdown(); this.initGlDropdown();
this.initRemoveDueDate(); this.initRemoveDueDate();
this.initDatePicker(); this.initDatePicker();
this.initStopPropagation();
} }
initGlDropdown() { initGlDropdown() {
this.$dropdown.glDropdown({ this.$dropdown.glDropdown({
opened: () => {
const calendar = this.$datePicker.data('pikaday');
calendar.show();
},
hidden: () => { hidden: () => {
this.$selectbox.hide(); this.$selectbox.hide();
this.$value.css('display', ''); this.$value.css('display', '');
...@@ -38,25 +43,37 @@ ...@@ -38,25 +43,37 @@
} }
initDatePicker() { initDatePicker() {
this.$datePicker.datepicker({ const $dueDateInput = $(`input[name='${this.fieldName}']`);
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='" + this.fieldName + "']").val(), const calendar = new Pikaday({
altField: "input[name='" + this.fieldName + "']", field: $dueDateInput.get(0),
onSelect: () => { theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
$dueDateInput.val(formattedDate);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) { if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue(); this.updateIssueBoardIssue();
} else { } else {
return this.saveDueDate(true); this.saveDueDate(true);
} }
} }
}); });
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
} }
initRemoveDueDate() { initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => { this.$block.on('click', '.js-remove-due-date', (e) => {
const calendar = this.$datePicker.data('pikaday');
e.preventDefault(); e.preventDefault();
calendar.setDate(null);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) { if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue(); this.updateIssueBoardIssue();
...@@ -67,12 +84,6 @@ ...@@ -67,12 +84,6 @@
}); });
} }
initStopPropagation() {
$(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
return e.stopImmediatePropagation();
});
}
saveDueDate(isDropdown) { saveDueDate(isDropdown) {
this.parseSelectedDate(); this.parseSelectedDate();
this.prepSelectedDate(); this.prepSelectedDate();
...@@ -86,7 +97,7 @@ ...@@ -86,7 +97,7 @@
// Construct Date object manually to avoid buggy dateString support within Date constructor // Construct Date object manually to avoid buggy dateString support within Date constructor
const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
} else { } else {
this.displayedDate = 'No due date'; this.displayedDate = 'No due date';
} }
...@@ -153,14 +164,24 @@ ...@@ -153,14 +164,24 @@
} }
initMilestoneDatePicker() { initMilestoneDatePicker() {
$('.datepicker').datepicker({ $('.datepicker').each(function() {
dateFormat: 'yy-mm-dd' const $datePicker = $(this);
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
$datePicker.data('pikaday', calendar);
}); });
$('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault(); e.preventDefault();
const datepicker = $(e.target).siblings('.datepicker'); const calendar = $(e.target).siblings('.datepicker').data('pikaday');
$.datepicker._clearDate(datepicker); calendar.setDate(null);
}); });
} }
......
...@@ -437,7 +437,7 @@ ...@@ -437,7 +437,7 @@
} }
}; };
GitLabDropdown.prototype.opened = function() { GitLabDropdown.prototype.opened = function(e) {
var contentHtml; var contentHtml;
this.resetRows(); this.resetRows();
this.addArrowKeyEvent(); this.addArrowKeyEvent();
...@@ -457,6 +457,10 @@ ...@@ -457,6 +457,10 @@
this.positionMenuAbove(); this.positionMenuAbove();
} }
if (this.options.opened) {
this.options.opened.call(this, e);
}
return this.dropdown.trigger('shown.gl.dropdown'); return this.dropdown.trigger('shown.gl.dropdown');
}; };
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
/* global ZenMode */ /* global ZenMode */
/* global Autosave */ /* global Autosave */
/* global GroupsSelect */ /* global GroupsSelect */
/* global dateFormat */
/* global Pikaday */
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -14,7 +16,7 @@ ...@@ -14,7 +16,7 @@
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) {
var $issuableDueDate; var $issuableDueDate, calendar;
this.form = form; this.form = form;
this.toggleWip = bind(this.toggleWip, this); this.toggleWip = bind(this.toggleWip, this);
this.renderWipExplanation = bind(this.renderWipExplanation, this); this.renderWipExplanation = bind(this.renderWipExplanation, this);
...@@ -37,12 +39,14 @@ ...@@ -37,12 +39,14 @@
this.initMoveDropdown(); this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date'); $issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) { if ($issuableDueDate.length) {
$('.datepicker').datepicker({ calendar = new Pikaday({
dateFormat: 'yy-mm-dd', field: $issuableDueDate.get(0),
onSelect: function(dateText, inst) { theme: 'gitlab-theme',
return $issuableDueDate.val(dateText); format: 'YYYY-MM-DD',
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())); });
} }
} }
......
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */ /* global Flash */
/* global Sortable */
((global) => { ((global) => {
class LabelManager { class LabelManager {
...@@ -9,11 +10,12 @@ ...@@ -9,11 +10,12 @@
this.otherLabels = otherLabels || $('.js-other-labels'); this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time'; this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state'); this.emptyState = document.querySelector('#js-priority-labels-empty-state');
this.prioritizedLabels.sortable({ this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
items: 'li', filter: '.empty-message',
placeholder: 'list-placeholder', forceFallback: true,
axis: 'y', fallbackClass: 'is-dragging',
update: this.onPrioritySortUpdate.bind(this) dataIdAttr: 'data-id',
onUpdate: this.onPrioritySortUpdate.bind(this),
}); });
this.bindEvents(); this.bindEvents();
} }
...@@ -51,13 +53,13 @@ ...@@ -51,13 +53,13 @@
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
} }
if ($from.find('li').length === 1) { $label.detach().appendTo($target);
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden'); $from.find('.empty-message').removeClass('hidden');
} }
if (!$target.find('li').length) { if ($target.find('> li:not(.empty-message)').length) {
$target.find('.empty-message').addClass('hidden'); $target.find('.empty-message').addClass('hidden');
} }
$label.detach().appendTo($target);
// Return if we are not persisting state // Return if we are not persisting state
if (!persistState) { if (!persistState) {
return; return;
...@@ -101,8 +103,12 @@ ...@@ -101,8 +103,12 @@
getSortedLabelsIds() { getSortedLabelsIds() {
const sortedIds = []; const sortedIds = [];
this.prioritizedLabels.find('li').each(function() { this.prioritizedLabels.find('> li').each(function() {
sortedIds.push($(this).data('id')); const id = $(this).data('id');
if (id) {
sortedIds.push(id);
}
}); });
return sortedIds; return sortedIds;
} }
......
/* global Pikaday */
/* global dateFormat */
(() => { (() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are // Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling // children of an element with the `clearable-input` class, and have a sibling
...@@ -11,21 +13,34 @@ ...@@ -11,21 +13,34 @@
} }
const inputs = $(selector); const inputs = $(selector);
inputs.datepicker({ inputs.each((i, el) => {
dateFormat: 'yy-mm-dd', const $input = $(el);
minDate: 1,
onSelect: function onSelect() { const calendar = new Pikaday({
$(this).trigger('change'); field: $input.get(0),
toggleClearInput.call(this); theme: 'gitlab-theme',
}, format: 'YYYY-MM-DD',
minDate: new Date(),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
$input.trigger('change');
toggleClearInput.call($input);
},
});
$input.data('pikaday', calendar);
}); });
inputs.next('.js-clear-input').on('click', function clicked(event) { inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault(); event.preventDefault();
const input = $(this).closest('.clearable-input').find(selector); const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null) const calendar = input.data('pikaday');
.trigger('change');
calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input); toggleClearInput.call(input);
}); });
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */ /* global Flash */
/* global Sortable */
(function() { (function() {
this.Milestone = (function() { this.Milestone = (function() {
...@@ -8,11 +9,9 @@ ...@@ -8,11 +9,9 @@
type: "PUT", type: "PUT",
url: issue_url, url: issue_url,
data: data, data: data,
success: (function(_this) { success: function(_data) {
return function(_data) { return Milestone.successCallback(_data, li);
return _this.successCallback(_data, li); },
};
})(this),
error: function(data) { error: function(data) {
return new Flash("Issue update failed", 'alert'); return new Flash("Issue update failed", 'alert');
}, },
...@@ -27,11 +26,9 @@ ...@@ -27,11 +26,9 @@
type: "PUT", type: "PUT",
url: sort_issues_url, url: sort_issues_url,
data: data, data: data,
success: (function(_this) { success: function(_data) {
return function(_data) { return Milestone.successCallback(_data);
return _this.successCallback(_data); },
};
})(this),
error: function() { error: function() {
return new Flash("Issues update failed", 'alert'); return new Flash("Issues update failed", 'alert');
}, },
...@@ -46,11 +43,9 @@ ...@@ -46,11 +43,9 @@
type: "PUT", type: "PUT",
url: sort_mr_url, url: sort_mr_url,
data: data, data: data,
success: (function(_this) { success: function(_data) {
return function(_data) { return Milestone.successCallback(_data);
return _this.successCallback(_data); },
};
})(this),
error: function(data) { error: function(data) {
return new Flash("Issue update failed", 'alert'); return new Flash("Issue update failed", 'alert');
}, },
...@@ -63,11 +58,9 @@ ...@@ -63,11 +58,9 @@
type: "PUT", type: "PUT",
url: merge_request_url, url: merge_request_url,
data: data, data: data,
success: (function(_this) { success: function(_data) {
return function(_data) { return Milestone.successCallback(_data, li);
return _this.successCallback(_data, li); },
};
})(this),
error: function(data) { error: function(data) {
return new Flash("Issue update failed", 'alert'); return new Flash("Issue update failed", 'alert');
}, },
...@@ -81,65 +74,30 @@ ...@@ -81,65 +74,30 @@
img_tag = $('<img/>'); img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url); img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16'); img_tag.addClass('avatar s16');
$(element).find('.assignee-icon').html(img_tag); $(element).find('.assignee-icon img').replaceWith(img_tag);
} else { } else {
$(element).find('.assignee-icon').html(''); $(element).find('.assignee-icon').empty();
} }
return $(element).effect('highlight'); return $(element).effect('highlight');
}; };
function Milestone() { function Milestone() {
var oldMouseStart; var oldMouseStart;
oldMouseStart = $.ui.sortable.prototype._mouseStart;
$.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
this._trigger("beforeStart", event, this._uiHash());
return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
};
this.bindIssuesSorting(); this.bindIssuesSorting();
this.bindMergeRequestSorting(); this.bindMergeRequestSorting();
this.bindTabsSwitching(); this.bindTabsSwitching();
} }
Milestone.prototype.bindIssuesSorting = function() { Milestone.prototype.bindIssuesSorting = function() {
return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({ $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
connectWith: ".issues-sortable-list", this.createSortable(el, {
dropOnEmpty: true, group: 'issue-list',
items: "li:not(.ui-sort-disabled)", listEls: $('.issues-sortable-list'),
beforeStart: function(event, ui) { fieldName: 'issue',
return $(".issues-sortable-list").css("min-height", ui.item.outerHeight()); sortCallback: Milestone.sortIssues,
}, updateCallback: Milestone.updateIssue,
stop: function(event, ui) { });
return $(".issues-sortable-list").css("min-height", "0px"); }.bind(this));
},
update: function(event, ui) {
var data;
// Prevents sorting from container which element has been removed.
if ($(this).find(ui.item).length > 0) {
data = $(this).sortable("serialize");
return Milestone.sortIssues(data);
}
},
receive: function(event, ui) {
var data, issue_id, issue_url, new_state;
new_state = $(this).data('state');
issue_id = ui.item.data('iid');
issue_url = ui.item.data('url');
data = (function() {
switch (new_state) {
case 'ongoing':
return "issue[assignee_id]=" + gon.current_user_id;
case 'unassigned':
return "issue[assignee_id]=";
case 'closed':
return "issue[state_event]=close";
}
})();
if ($(ui.sender).data('state') === "closed") {
data += "&issue[state_event]=reopen";
}
return Milestone.updateIssue(ui.item, issue_url, data);
}
}).disableSelection();
}; };
Milestone.prototype.bindTabsSwitching = function() { Milestone.prototype.bindTabsSwitching = function() {
...@@ -154,42 +112,62 @@ ...@@ -154,42 +112,62 @@
}; };
Milestone.prototype.bindMergeRequestSorting = function() { Milestone.prototype.bindMergeRequestSorting = function() {
return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({ $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
connectWith: ".merge_requests-sortable-list", this.createSortable(el, {
dropOnEmpty: true, group: 'merge-request-list',
items: "li:not(.ui-sort-disabled)", listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
beforeStart: function(event, ui) { fieldName: 'merge_request',
return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight()); sortCallback: Milestone.sortMergeRequests,
updateCallback: Milestone.updateMergeRequest,
});
}.bind(this));
};
Milestone.prototype.createSortable = function(el, opts) {
return Sortable.create(el, {
group: opts.group,
filter: '.is-disabled',
forceFallback: true,
onStart: function(e) {
opts.listEls.css('min-height', e.item.offsetHeight);
}, },
stop: function(event, ui) { onEnd: function () {
return $(".merge_requests-sortable-list").css("min-height", "0px"); opts.listEls.css("min-height", "0px");
}, },
update: function(event, ui) { onUpdate: function(e) {
var data; var ids = this.toArray(),
data = $(this).sortable("serialize"); data;
return Milestone.sortMergeRequests(data);
if (ids.length) {
data = ids.map(function(id) {
return 'sortable_' + opts.fieldName + '[]=' + id;
}).join('&');
opts.sortCallback(data);
}
}, },
receive: function(event, ui) { onAdd: function (e) {
var data, merge_request_id, merge_request_url, new_state; var data, issuableId, issuableUrl, newState;
new_state = $(this).data('state'); newState = e.to.dataset.state;
merge_request_id = ui.item.data('iid'); issuableUrl = e.item.dataset.url;
merge_request_url = ui.item.data('url');
data = (function() { data = (function() {
switch (new_state) { switch (newState) {
case 'ongoing': case 'ongoing':
return "merge_request[assignee_id]=" + gon.current_user_id; return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
case 'unassigned': case 'unassigned':
return "merge_request[assignee_id]="; return opts.fieldName + '[assignee_id]=';
case 'closed': case 'closed':
return "merge_request[state_event]=close"; return opts.fieldName + '[state_event]=close';
} }
})(); })();
if ($(ui.sender).data('state') === "closed") { if (e.from.dataset.state === 'closed') {
data += "&merge_request[state_event]=reopen"; data += '&' + opts.fieldName + '[state_event]=reopen';
} }
return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
opts.updateCallback(e.item, issuableUrl, data);
this.options.onUpdate.call(this, e);
} }
}).disableSelection(); });
}; };
return Milestone; return Milestone;
......
...@@ -50,14 +50,15 @@ ...@@ -50,14 +50,15 @@
return ( return (
children[target.index] || children[target.index] ||
children[target.index === 'first' ? 0 : -1] || children[target.index === 'first' ? 0 : -1] ||
children[target.index === 'last' ? children.length - 1 : -1] children[target.index === 'last' ? children.length - 1 : -1] ||
el
); );
} }
function getRect(el) { function getRect(el) {
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
var width = rect.right - rect.left; var width = rect.right - rect.left;
var height = rect.bottom - rect.top; var height = rect.bottom - rect.top + 10;
return { return {
x: rect.left, x: rect.left,
......
...@@ -111,7 +111,7 @@ require('./commit'); ...@@ -111,7 +111,7 @@ require('./commit');
* If provided, returns the commit ref. * If provided, returns the commit ref.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* Matched `url` prop sent in the API to `path` prop needed * Matches `path` prop sent in the API to `ref_url` prop needed
* in the commit component. * in the commit component.
* *
* @returns {Object|Undefined} * @returns {Object|Undefined}
...@@ -119,8 +119,8 @@ require('./commit'); ...@@ -119,8 +119,8 @@ require('./commit');
commitRef() { commitRef() {
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'url') { if (prop === 'path') {
accumulator.path = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory * This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope. * the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery-ui/datepicker
*= require jquery-ui/autocomplete *= require jquery-ui/autocomplete
*= require jquery.atwho *= require jquery.atwho
*= require select2 *= require select2
...@@ -19,6 +18,8 @@ ...@@ -19,6 +18,8 @@
* directory. * directory.
*/ */
@import "../../../node_modules/pikaday/scss/pikaday";
/* /*
* GitLab UI framework * GitLab UI framework
*/ */
......
...@@ -43,3 +43,56 @@ ...@@ -43,3 +43,56 @@
float: right; float: right;
font-size: 12px; font-size: 12px;
} }
.pika-single.gitlab-theme {
.pika-label {
color: $gl-text-color-secondary;
font-size: 14px;
font-weight: normal;
}
th {
padding: 2px 0;
color: $note-disabled-comment-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
abbr {
cursor: default;
}
td {
border: 1px solid $calendar-border-color;
&:first-child {
border-left: 0;
}
&:last-child {
border-right: 0;
}
}
.pika-day {
border-radius: 0;
background-color: $white-light;
text-align: center;
}
.is-today {
.pika-day {
color: inherit;
font-weight: normal;
}
}
.is-selected .pika-day,
.pika-day:hover,
.is-today .pika-day:hover {
background: $gl-primary;
color: $white-light;
box-shadow: none;
}
}
...@@ -506,119 +506,16 @@ ...@@ -506,119 +506,16 @@
max-height: 230px; max-height: 230px;
} }
.ui-widget { .pika-single {
table { position: relative!important;
margin: 0; top: 0!important;
} border: 0;
box-shadow: none;
&.ui-datepicker-inline {
padding: 0 10px;
border: 0;
width: 100%;
}
.ui-datepicker-header {
padding: 0 8px 10px;
border: 0;
.ui-icon {
background: none;
font-size: 20px;
text-indent: 0;
&::before {
display: block;
position: relative;
top: -2px;
color: $dropdown-title-btn-color;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
}
.ui-datepicker-calendar {
.ui-state-hover,
.ui-state-active {
color: $white-light;
border: 0;
}
}
.ui-datepicker-prev,
.ui-datepicker-next {
top: 0;
height: 15px;
cursor: pointer;
&:hover {
background-color: transparent;
border: 0;
.ui-icon::before {
color: $md-link-color;
}
}
}
.ui-datepicker-prev {
left: 0;
.ui-icon::before {
content: '\f104';
text-align: left;
}
}
.ui-datepicker-next {
right: 0;
.ui-icon::before {
content: '\f105';
text-align: right;
}
}
td {
padding: 0;
border: 1px solid $calendar-border-color;
&:first-child {
border-left: 0;
}
&:last-child {
border-right: 0;
}
a {
line-height: 17px;
border: 0;
border-radius: 0;
}
}
.ui-datepicker-title {
color: $gl-text-color;
font-size: 14px;
line-height: 1;
font-weight: normal;
}
}
th {
padding: 2px 0;
color: $note-disabled-comment-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
} }
.ui-datepicker-unselectable { .pika-lendar {
background-color: $gray-light; margin-top: -5px;
margin-bottom: 0;
} }
} }
......
...@@ -2,42 +2,6 @@ ...@@ -2,42 +2,6 @@
font-family: $regular_font; font-family: $regular_font;
font-size: $font-size-base; font-size: $font-size-base;
&.ui-datepicker,
&.ui-datepicker-inline {
border: 1px solid $jq-ui-border;
padding: 10px;
width: 270px;
.ui-datepicker-header {
background: $white-light;
border-color: $jq-ui-border;
.ui-datepicker-prev,
.ui-datepicker-next {
top: 4px;
}
.ui-datepicker-prev {
left: 2px;
}
.ui-datepicker-next {
right: 2px;
}
.ui-state-hover {
background: transparent;
border: 0;
cursor: pointer;
}
}
.ui-datepicker-calendar td a {
padding: 5px;
text-align: center;
}
}
&.ui-autocomplete { &.ui-autocomplete {
border-color: $jq-ui-border; border-color: $jq-ui-border;
padding: 0; padding: 0;
...@@ -59,25 +23,4 @@ ...@@ -59,25 +23,4 @@
border: 0; border: 0;
background: transparent; background: transparent;
} }
.ui-datepicker-calendar {
.ui-state-active,
.ui-state-hover,
.ui-state-focus {
border: 1px solid $gl-primary;
background: $gl-primary;
color: $white-light;
}
}
}
.ui-sortable-handle {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
&:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
} }
...@@ -116,6 +116,22 @@ ...@@ -116,6 +116,22 @@
} }
.manage-labels-list { .manage-labels-list {
> li:not(.empty-message) {
background-color: $white-light;
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
&:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
&.sortable-ghost {
opacity: 0.3;
}
}
.btn-action { .btn-action {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -125,6 +125,12 @@ ...@@ -125,6 +125,12 @@
line-height: 16px; line-height: 16px;
} }
@media (min-width: $screen-sm-min) {
.stage-cell {
padding: 0 4px;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
order: 1; order: 1;
margin-top: $gl-padding-top; margin-top: $gl-padding-top;
......
...@@ -178,3 +178,9 @@ ...@@ -178,3 +178,9 @@
} }
} }
} }
.issuable-row {
background-color: $white-light;
cursor: -webkit-grab;
cursor: grab;
}
...@@ -201,10 +201,6 @@ ...@@ -201,10 +201,6 @@
color: $note-disabled-comment-color; color: $note-disabled-comment-color;
} }
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
text-align: center;
}
.created-personal-access-token-container { .created-personal-access-token-container {
#created-personal-access-token { #created-personal-access-token {
width: 90%; width: 90%;
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
def index def index
@projects = Project.limit(10) @projects = Project.with_route.limit(10)
@users = User.limit(10) @users = User.limit(10)
@groups = Group.limit(10) @groups = Group.with_route.limit(10)
@license = License.current @license = License.current
end end
end end
...@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
@groups = Group.with_statistics @groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
...@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def destroy def destroy
DestroyGroupService.new(@group, current_user).async_execute Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion." redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end end
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@group_members = current_user.group_members.includes(:source).page(params[:page]) @group_members = current_user.group_members.includes(source: :route).page(params[:page])
end end
end end
...@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController ...@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new, :create] before_action :authorize_create_group!, only: [:new, :create]
# Load group projects # Load group projects
before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity] before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups]
layout :determine_layout layout :determine_layout
def index def index
...@@ -37,13 +39,6 @@ class GroupsController < Groups::ApplicationController ...@@ -37,13 +39,6 @@ class GroupsController < Groups::ApplicationController
end end
def show def show
if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
@nested_groups = group.children
setup_projects setup_projects
respond_to do |format| respond_to do |format|
...@@ -62,6 +57,11 @@ class GroupsController < Groups::ApplicationController ...@@ -62,6 +57,11 @@ class GroupsController < Groups::ApplicationController
end end
end end
def subgroups
@nested_groups = group.children
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
def activity def activity
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController ...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
end end
def destroy def destroy
DestroyGroupService.new(@group, current_user).async_execute Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion." redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end end
...@@ -99,13 +99,16 @@ class GroupsController < Groups::ApplicationController ...@@ -99,13 +99,16 @@ class GroupsController < Groups::ApplicationController
protected protected
def setup_projects def setup_projects
options = {}
options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1'
@projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity @projects = @projects.sorted_by_activity
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank? @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
@shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
end end
def authorize_create_group! def authorize_create_group!
...@@ -138,7 +141,8 @@ class GroupsController < Groups::ApplicationController ...@@ -138,7 +141,8 @@ class GroupsController < Groups::ApplicationController
:public, :public,
:request_access_enabled, :request_access_enabled,
:share_with_group_lock, :share_with_group_lock,
:visibility_level :visibility_level,
:parent_id
] ]
end end
...@@ -154,4 +158,11 @@ class GroupsController < Groups::ApplicationController ...@@ -154,4 +158,11 @@ class GroupsController < Groups::ApplicationController
@events = event_filter.apply_filter(@events).with_associations @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
def user_actions
if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
end end
...@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object| objects.each do |object|
if existing_oids.include?(object[:oid]) if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object) object[:actions] = download_actions(object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else else
object[:error] = { object[:error] = {
code: 404, code: 404,
......
...@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy def destroy
if note.editable? if note.editable?
Notes::DeleteService.new(project, current_user).execute(note) Notes::DestroyService.new(project, current_user).execute(note)
end end
respond_to do |format| respond_to do |format|
......
...@@ -24,7 +24,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -24,7 +24,7 @@ class RegistrationsController < Devise::RegistrationsController
end end
def destroy def destroy
DeleteUserService.new(current_user).execute(current_user) Users::DestroyService.new(current_user).execute(current_user)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder ...@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder
def execute(current_user = nil) def execute(current_user = nil)
segments = all_groups(current_user) segments = all_groups(current_user)
find_union(segments, Group).order_id_desc find_union(segments, Group).with_route.order_id_desc
end end
private private
......
...@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder ...@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder
segments = all_projects(current_user) segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
find_union(segments, Project) find_union(segments, Project).with_route
end end
private private
......
...@@ -64,11 +64,11 @@ module MergeRequestsHelper ...@@ -64,11 +64,11 @@ module MergeRequestsHelper
end end
def mr_closes_issues def mr_closes_issues
@mr_closes_issues ||= @merge_request.closes_issues @mr_closes_issues ||= @merge_request.closes_issues(current_user)
end end
def mr_issues_mentioned_but_not_closing def mr_issues_mentioned_but_not_closing
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end end
def mr_change_branches_path(merge_request) def mr_change_branches_path(merge_request)
......
...@@ -86,7 +86,9 @@ module TodosHelper ...@@ -86,7 +86,9 @@ module TodosHelper
[ [
{ id: '', text: 'Any Action' }, { id: '', text: 'Any Action' },
{ id: Todo::ASSIGNED, text: 'Assigned' }, { id: Todo::ASSIGNED, text: 'Assigned' },
{ id: Todo::MENTIONED, text: 'Mentioned' } { id: Todo::MENTIONED, text: 'Mentioned' },
{ id: Todo::MARKED, text: 'Added' },
{ id: Todo::BUILD_FAILED, text: 'Pipelines' }
] ]
end end
......
...@@ -21,7 +21,7 @@ module Ci ...@@ -21,7 +21,7 @@ module Ci
end end
serialize :options serialize :options
serialize :yaml_variables, Gitlab::Serialize::Ci::Variables serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
validates :coverage, numericality: true, allow_blank: true validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref validates_presence_of :ref
......
# Store object full path in separate table for easy lookup and uniq validation # Store object full path in separate table for easy lookup and uniq validation
# Object must have path db field and respond to full_path and full_path_changed? methods. # Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable module Routable
extend ActiveSupport::Concern extend ActiveSupport::Concern
...@@ -9,7 +9,13 @@ module Routable ...@@ -9,7 +9,13 @@ module Routable
validates_associated :route validates_associated :route
validates :route, presence: true validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed? scope :with_route, -> { includes(:route) }
before_validation do
if full_path_changed? || full_name_changed?
prepare_route
end
end
end end
class_methods do class_methods do
...@@ -77,10 +83,62 @@ module Routable ...@@ -77,10 +83,62 @@ module Routable
end end
end end
def full_name
if route && route.name.present?
@full_name ||= route.name
else
update_route if persisted?
build_full_name
end
end
def full_path
if route && route.path.present?
@full_path ||= route.path
else
update_route if persisted?
build_full_path
end
end
private private
def update_route_path def full_name_changed?
name_changed? || parent_changed?
end
def full_path_changed?
path_changed? || parent_changed?
end
def build_full_name
if parent && name
parent.human_name + ' / ' + name
else
name
end
end
def build_full_path
if parent && path
parent.full_path + '/' + path
else
path
end
end
def update_route
prepare_route
route.save
end
def prepare_route
route || build_route(source: self) route || build_route(source: self)
route.path = full_path route.path = build_full_path
route.name = build_full_name
@full_path = nil
@full_name = nil
end end
end end
...@@ -569,7 +569,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -569,7 +569,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately. # running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources. # This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author) def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker? return if project.has_external_issue_tracker?
transaction do transaction do
...@@ -581,10 +581,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -581,10 +581,6 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def closes_issue?(issue)
closes_issues.include?(issue)
end
# Return the set of issues that will be closed if this merge request is accepted. # Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author) def closes_issues(current_user = self.author)
if target_branch == project.default_branch if target_branch == project.default_branch
...@@ -598,13 +594,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -598,13 +594,13 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def issues_mentioned_but_not_closing(current_user = self.author) def issues_mentioned_but_not_closing(current_user)
return [] unless target_branch == project.default_branch return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description) ext.analyze(description)
ext.issues - closes_issues ext.issues - closes_issues(current_user)
end end
def target_project_path def target_project_path
......
...@@ -8,6 +8,11 @@ class Namespace < ActiveRecord::Base ...@@ -8,6 +8,11 @@ class Namespace < ActiveRecord::Base
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Routable include Routable
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
# Android repo (15) + some extra backup.
NUMBER_OF_ANCESTORS_ALLOWED = 20
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
...@@ -30,6 +35,8 @@ class Namespace < ActiveRecord::Base ...@@ -30,6 +35,8 @@ class Namespace < ActiveRecord::Base
length: { maximum: 255 }, length: { maximum: 255 },
namespace: true namespace: true
validate :nesting_level_allowed
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed? after_update :move_dir, if: :path_changed?
...@@ -175,31 +182,14 @@ class Namespace < ActiveRecord::Base ...@@ -175,31 +182,14 @@ class Namespace < ActiveRecord::Base
current_application_settings.repository_size_limit current_application_settings.repository_size_limit
end end
def full_path
if parent
parent.full_path + '/' + path
else
path
end
end
def shared_runners_enabled? def shared_runners_enabled?
projects.with_shared_runners.any? projects.with_shared_runners.any?
end end
def full_name
@full_name ||=
if parent
parent.full_name + ' / ' + name
else
name
end
end
# Scopes the model on ancestors of the record # Scopes the model on ancestors of the record
def ancestors def ancestors
if parent_id if parent_id
path = route.path path = route ? route.path : full_path
paths = [] paths = []
until path.blank? until path.blank?
...@@ -222,6 +212,10 @@ class Namespace < ActiveRecord::Base ...@@ -222,6 +212,10 @@ class Namespace < ActiveRecord::Base
[owner_id] [owner_id]
end end
def parent_changed?
parent_id_changed?
end
private private
def repository_storage_paths def repository_storage_paths
...@@ -260,10 +254,6 @@ class Namespace < ActiveRecord::Base ...@@ -260,10 +254,6 @@ class Namespace < ActiveRecord::Base
find_each(&:refresh_members_authorized_projects) find_each(&:refresh_members_authorized_projects)
end end
def full_path_changed?
path_changed? || parent_id_changed?
end
def remove_exports! def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end end
...@@ -279,4 +269,10 @@ class Namespace < ActiveRecord::Base ...@@ -279,4 +269,10 @@ class Namespace < ActiveRecord::Base
path_was path_was
end end
end end
def nesting_level_allowed
if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
errors.add(:parent_id, "has too deep level of nesting")
end
end
end end
...@@ -250,7 +250,12 @@ class Project < ActiveRecord::Base ...@@ -250,7 +250,12 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) } scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) { joins(:route).where('routes.path LIKE ?', "#{path}/%") } scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
# includes(:route) which we use in ProjectsFinder.
joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'").
where('rs.path LIKE ?', "#{path}/%")
end
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
...@@ -915,26 +920,6 @@ class Project < ActiveRecord::Base ...@@ -915,26 +920,6 @@ class Project < ActiveRecord::Base
end end
end end
def name_with_namespace
@name_with_namespace ||= begin
if namespace
namespace.human_name + ' / ' + name
else
name
end
end
end
alias_method :human_name, :name_with_namespace
def full_path
if namespace && path
namespace.full_path + '/' + path
else
path
end
end
alias_method :path_with_namespace, :full_path
def execute_hooks(data, hooks_scope = :push_hooks) def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook| hooks.send(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s) hook.async_execute(data, hooks_scope.to_s)
...@@ -1570,6 +1555,18 @@ class Project < ActiveRecord::Base ...@@ -1570,6 +1555,18 @@ class Project < ActiveRecord::Base
map.public_path_for_source_path(path) map.public_path_for_source_path(path)
end end
def parent
namespace
end
def parent_changed?
namespace_id_changed?
end
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path
private private
def cross_namespace_reference?(from) def cross_namespace_reference?(from)
...@@ -1597,8 +1594,15 @@ class Project < ActiveRecord::Base ...@@ -1597,8 +1594,15 @@ class Project < ActiveRecord::Base
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end end
def full_path_changed? # Similar to the normal callbacks that hook into the life cycle of an
path_changed? || namespace_id_changed? # Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
# callbacks throw an exception, the object will not be added to the
# collection. Before you add a new board to the boards collection if you
# already have 1, 2, or n it will fail, but it if you have 0 that is lower
# than the number of permitted boards per project it won't fail.
def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end end
def update_project_statistics def update_project_statistics
......
...@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base ...@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
after_update :rename_descendants, if: :path_changed? after_update :rename_descendants
def rename_descendants def rename_descendants
# We update each row separately because MySQL does not have regexp_replace. if path_changed? || name_changed?
# rubocop:disable Rails/FindEach descendants = Route.where('path LIKE ?', "#{path_was}/%")
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks. descendants.each do |route|
# We need this to avoid recursive call of rename_descendants method attributes = {}
route.update_column(:path, route.path.sub(path_was, path))
if path_changed? && route.path.present?
attributes[:path] = route.path.sub(path_was, path)
end
if name_changed? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
# Note that update_columns skips validation and callbacks.
# We need this to avoid recursive call of rename_descendants method
route.update_columns(attributes) unless attributes.empty?
end
end end
# rubocop:enable Rails/FindEach
end end
end end
...@@ -130,7 +130,7 @@ class User < ActiveRecord::Base ...@@ -130,7 +130,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :signup_domain_valid?, on: :create before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
......
class EnvironmentSerializer < BaseSerializer class EnvironmentSerializer < BaseSerializer
Item = Struct.new(:name, :size, :latest)
entity EnvironmentEntity entity EnvironmentEntity
def within_folders
tap { @itemize = true }
end
def with_pagination(request, response)
tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
def itemized?
@itemize
end
def paginated?
@paginator.present?
end
def represent(resource, opts = {})
resource = @paginator.paginate(resource) if paginated?
if itemized?
itemize(resource).map do |item|
{ name: item.name,
size: item.size,
latest: super(item.latest, opts) }
end
else
super(resource, opts)
end
end
private
def itemize(resource)
items = resource.group(:item_name).order('item_name ASC')
.pluck('COALESCE(environment_type, name) AS item_name',
'COUNT(*) AS environments_count',
'MAX(id) AS last_environment_id')
environments = resource.where(id: items.map(&:last)).index_by(&:id)
items.map do |name, size, id|
Item.new(name, size, environments[id])
end
end
end end
class PipelineSerializer < BaseSerializer class PipelineSerializer < BaseSerializer
class InvalidResourceError < StandardError; end class InvalidResourceError < StandardError; end
include API::Helpers::Pagination
Struct.new('Pagination', :request, :response)
entity PipelineEntity entity PipelineEntity
def represent(resource, opts = {})
if paginated?
raise InvalidResourceError unless resource.respond_to?(:page)
super(paginate(resource.includes(project: :namespace)), opts)
else
super(resource, opts)
end
end
def paginated?
defined?(@pagination)
end
def with_pagination(request, response) def with_pagination(request, response)
tap { @pagination = Struct::Pagination.new(request, response) } tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end end
private def paginated?
@paginator.present?
# Methods needed by `API::Helpers::Pagination`
#
def params
@pagination.request.query_parameters
end end
def request def represent(resource, opts = {})
@pagination.request if resource.is_a?(ActiveRecord::Relation)
end resource = resource.includes(project: :namespace)
end
def header(header, value) if paginated?
@pagination.response.headers[header] = value super(@paginator.paginate(resource), opts)
else
super(resource, opts)
end
end end
end end
class DeleteUserService
attr_accessor :current_user
def initialize(current_user)
@current_user = current_user
end
def execute(user, options = {})
if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user
end
user.solo_owned_groups.each do |group|
DestroyGroupService.new(group, current_user).execute
end
user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
user_data = user.destroy
namespace.really_destroy!
user_data
end
end
class DestroyGroupService
attr_accessor :group, :current_user
def initialize(group, user)
@group, @current_user = group, user
end
def async_execute
# Soft delete via paranoia gem
group.destroy
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
group.projects.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
group.children.each do |group|
DestroyGroupService.new(group, current_user).async_execute
end
group.really_destroy!
end
end
module Groups
class DestroyService < Groups::BaseService
def async_execute
# Soft delete via paranoia gem
group.destroy
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
group.projects.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
group.children.each do |group|
DestroyService.new(group, current_user).async_execute
end
group.really_destroy!
end
end
end
module Notes module Notes
class DeleteService < BaseService class DestroyService < BaseService
def execute(note) def execute(note)
note.destroy note.destroy
end end
......
module Users
class DestroyService
attr_accessor :current_user
def initialize(current_user)
@current_user = current_user
end
def execute(user, options = {})
if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user
end
user.solo_owned_groups.each do |group|
Groups::DestroyService.new(group, current_user).execute
end
user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
user_data = user.destroy
namespace.really_destroy!
user_data
end
end
end
.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
%h1.group-title
@#{@group.path}
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
- if @group.description.present?
.group-home-desc
= markdown_field(@group, :description)
- if current_user
.group-buttons
= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
%ul.nav-links
= nav_link(page: group_path(@group)) do
= link_to group_path(@group) do
Projects
= nav_link(page: subgroups_group_path(@group)) do
= link_to subgroups_group_path(@group) do
Subgroups
= render "header_title" = render "header_title"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
= render 'shared/milestones/top', milestone: @milestone, group: @group = render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/summary', milestone: @milestone = render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
...@@ -4,38 +4,12 @@ ...@@ -4,38 +4,12 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.group-home-panel.text-center = render 'groups/home_panel'
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
%h1.group-title
@#{@group.path}
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
- if @group.description.present?
.group-home-desc
= markdown_field(@group, :description)
- if current_user
.group-buttons
= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
.groups-header{ class: container_class } .groups-header{ class: container_class }
.top-area .top-area
%ul.nav-links = render 'groups/show_nav'
%li.active
= link_to "#projects", 'data-toggle' => 'tab' do
All Projects
- if @shared_projects.present?
%li
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
- if @nested_groups.present?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Subgroups
.nav-controls .nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
...@@ -44,15 +18,4 @@ ...@@ -44,15 +18,4 @@
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
New Project New Project
.tab-content = render "projects", projects: @projects
.tab-pane.active#projects
= render "projects", projects: @projects
- if @shared_projects.present?
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
- if @nested_groups.present?
.tab-pane#groups
%ul.content-list
= render partial: 'shared/groups/group', collection: @nested_groups
- @no_container = true
= render 'groups/home_panel'
.groups-header{ class: container_class }
.top-area
= render 'groups/show_nav'
.nav-controls
= form_tag request.path, method: :get do |f|
= search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
- if can? current_user, :admin_group, @group
= link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
New Subgroup
- if @nested_groups.present?
%ul.content-list
= render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
- else
.nothing-here-block
There are no subgroups to show.
...@@ -85,11 +85,17 @@ ...@@ -85,11 +85,17 @@
:javascript :javascript
var date = $('#personal_access_token_expires_at').val(); var $dateField = $('#personal_access_token_expires_at');
var date = $dateField.val();
var datepicker = $(".datepicker").datepicker({
dateFormat: "yy-mm-dd", new Pikaday({
minDate: 0 field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
}); });
$("#created-personal-access-token").click(function() { $("#created-personal-access-token").click(function() {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('boards')
= page_specific_javascript_bundle_tag('boards_test') if Rails.env.test? = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
......
...@@ -22,9 +22,7 @@ ...@@ -22,9 +22,7 @@
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref) - elsif create_mr_button?(@repository.root_ref, @ref)
.control .control
= link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
= icon('plus')
Create Merge Request
.control .control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
......
...@@ -23,6 +23,4 @@ ...@@ -23,6 +23,4 @@
- if @merge_request.present? - if @merge_request.present?
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button? - elsif create_mr_button?
= link_to create_mr_path, class: 'prepend-left-10 btn' do = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn'
= icon("plus")
Create Merge Request
- content_for :note_actions do - content_for :note_actions do
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes #notes
= render 'projects/notes/notes_with_form' = render 'projects/notes/notes_with_form'
...@@ -35,9 +35,9 @@ ...@@ -35,9 +35,9 @@
= link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
%li %li
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li %li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin? - if @issue.submittable_as_spam? && current_user.admin?
...@@ -48,8 +48,8 @@ ...@@ -48,8 +48,8 @@
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue New issue
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if @issue.submittable_as_spam? && current_user.admin? - if @issue.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
- hide_class = '' - hide_class = ''
= render "projects/issues/head" = render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
- page_description @milestone.description - page_description @milestone.description
= render "projects/issues/head" = render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%div{ class: container_class } %div{ class: container_class }
.detail-page-header.milestone-page-header .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) } .status-box{ class: status_box_class(@milestone) }
......
- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
- if @group.persisted? - if @group.persisted?
.form-group .form-group
= f.label :name, class: 'control-label' do = f.label :name, class: 'control-label' do
...@@ -11,11 +12,15 @@ ...@@ -11,11 +12,15 @@
.col-sm-10 .col-sm-10
.input-group.gl-field-error-anchor .input-group.gl-field-error-anchor
.input-group-addon .input-group-addon
= root_url %span>= root_url
- if parent
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control', = f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true, autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
title: 'Please choose a group name with no special characters.' title: 'Please choose a group name with no special characters.'
- if parent
= f.hidden_field :parent_id, value: parent.id
- if @group.persisted? - if @group.persisted?
.alert.alert-warning.prepend-top-10 .alert.alert-warning.prepend-top-10
......
- group_member = local_assigns[:group_member] - group_member = local_assigns[:group_member]
- full_name = true unless local_assigns[:full_name] == false
- css_class = '' unless local_assigns[:css_class] - css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank? - css_class += " no-description" if group.description.blank?
...@@ -28,7 +29,10 @@ ...@@ -28,7 +29,10 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to group, class: 'group-name' do = link_to group, class: 'group-name' do
= group.full_name - if full_name
= group.full_name
- else
= group.name
- if group_member - if group_member
as as
......
...@@ -10,6 +10,3 @@ ...@@ -10,6 +10,3 @@
.col-sm-10 .col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
:javascript
new gl.DueDateSelectors();
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] - base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) - can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) } %li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
%span %span
- if show_project_name - if show_project_name
%strong #{project.name} &middot; %strong #{project.name} &middot;
......
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_recently_updated
- personal = params[:personal] - personal = params[:personal]
- archived = params[:archived] - archived = params[:archived]
- shared = params[:shared]
- namespace_id = params[:namespace_id] - namespace_id = params[:namespace_id]
.dropdown .dropdown
- toggle_text = projects_sort_options_hash[@sort] - toggle_text = projects_sort_options_hash[@sort]
...@@ -28,3 +29,14 @@ ...@@ -28,3 +29,14 @@
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
Owned by me Owned by me
- if @group && @group.shared_projects.present?
%li.divider
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do
All projects
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do
Hide shared projects
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do
Hide group projects
...@@ -6,6 +6,6 @@ class DeleteUserWorker ...@@ -6,6 +6,6 @@ class DeleteUserWorker
delete_user = User.find(delete_user_id) delete_user = User.find(delete_user_id)
current_user = User.find(current_user_id) current_user = User.find(current_user_id)
DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys) Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
end end
end end
...@@ -11,6 +11,6 @@ class GroupDestroyWorker ...@@ -11,6 +11,6 @@ class GroupDestroyWorker
user = User.find(user_id) user = User.find(user_id)
DestroyGroupService.new(group, user).execute Groups::DestroyService.new(group, user).execute
end end
end end
---
title: Remove plus icon from MR button on compare view
merge_request:
author:
---
title: Filter todos by manual add
merge_request: 8691
author: Jacopo Beschi @jacopo-beschi
---
title: Fixes Pipelines table is not showing branch name for commit
merge_request:
author:
---
title: Bypass email domain validation when a user is created by an admin.
merge_request: 8575
author: Reza Mohammadi @remohammadi
---
title: Allow creating nested groups via UI
merge_request: 8786
author:
---
title: Add nested groups to the API
merge_request: 9034
author:
---
title: Store group and project full name and full path in routes table
merge_request: 8979
author:
---
title: Support unauthenticated LFS object downloads for public projects
merge_request: 8824
author: Ben Boeckel
---
title: pass in current_user in MergeRequest and MergeRequestsHelper
merge_request: 8624
author: Dongqing Hu
---
title: 'removed unused parameter ''status_only: true'''
merge_request:
author:
---
title: Replaced jQuery UI datepicker
merge_request:
author:
---
title: Replaced jQuery UI sortable
merge_request:
author:
---
title: Fix inconsistent naming for services that delete things
merge_request: 5803
author: dixpac
...@@ -54,5 +54,6 @@ scope(path: 'groups/*id', ...@@ -54,5 +54,6 @@ scope(path: 'groups/*id',
get :merge_requests, as: :merge_requests_group get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group get :projects, as: :projects_group
get :activity, as: :activity_group get :activity, as: :activity_group
get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical get '/', action: :show, as: :group_canonical
end end
...@@ -17,7 +17,7 @@ var config = { ...@@ -17,7 +17,7 @@ var config = {
application: './application.js', application: './application.js',
blob_edit: './blob_edit/blob_edit_bundle.js', blob_edit: './blob_edit/blob_edit_bundle.js',
boards: './boards/boards_bundle.js', boards: './boards/boards_bundle.js',
boards_test: './boards/test_utils/simulate_drag.js', simulate_drag: './test_utils/simulate_drag.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
......
class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction! disable_ddl_transaction!
def change def up
add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false
end end
def down
remove_column :protected_branches, :developers_can_merge
end
end end
...@@ -14,7 +14,11 @@ class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration ...@@ -14,7 +14,11 @@ class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
def change def up
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
end end
def down
remove_column :spam_logs, :submitted_as_ham
end
end end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddNameToRoute < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :routes, :name, :string
end
end
...@@ -1221,6 +1221,7 @@ ActiveRecord::Schema.define(version: 20170207150212) do ...@@ -1221,6 +1221,7 @@ ActiveRecord::Schema.define(version: 20170207150212) do
t.string "path", null: false t.string "path", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "name"
end end
add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
......
...@@ -32,7 +32,8 @@ GET /groups ...@@ -32,7 +32,8 @@ GET /groups
"web_url": "http://localhost:3000/groups/foo-bar", "web_url": "http://localhost:3000/groups/foo-bar",
"request_access_enabled": false, "request_access_enabled": false,
"full_name": "Foobar Group", "full_name": "Foobar Group",
"full_path": "foo-bar" "full_path": "foo-bar",
"parent_id": null
} }
] ]
``` ```
...@@ -156,8 +157,9 @@ Example response: ...@@ -156,8 +157,9 @@ Example response:
"avatar_url": null, "avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter", "web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false, "request_access_enabled": false,
"full_name": "Foobar Group", "full_name": "Twitter",
"full_path": "foo-bar", "full_path": "twitter",
"parent_id": null,
"projects": [ "projects": [
{ {
"id": 7, "id": 7,
...@@ -350,6 +352,7 @@ Parameters: ...@@ -350,6 +352,7 @@ Parameters:
- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. - `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group - `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
- `request_access_enabled` (optional) - Allow users to request member access. - `request_access_enabled` (optional) - Allow users to request member access.
- `parent_id` (optional) - The parent group id for creating nested group.
## Transfer project to group ## Transfer project to group
...@@ -401,6 +404,7 @@ Example response: ...@@ -401,6 +404,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"full_name": "Foobar Group", "full_name": "Foobar Group",
"full_path": "foo-bar", "full_path": "foo-bar",
"parent_id": null,
"projects": [ "projects": [
{ {
"id": 9, "id": 9,
......
...@@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files ...@@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files
``` ```
```bash ```bash
curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
``` ```
Example response: Example response:
......
...@@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE ...@@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer. A clone of the project will be created in your computer.
>**Note:** If you clone your project via an URL that contains special characters, make sure that they are URL-encoded.
### Go into a project, directory or file to work in it ### Go into a project, directory or file to work in it
``` ```
......
...@@ -80,10 +80,13 @@ from step 5. ...@@ -80,10 +80,13 @@ from step 5.
1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console 1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
page from step 5. page from step 5.
1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
for the changes to take effect. installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be an Auth0 icon below the regular sign in On the sign in page there should now be an Auth0 icon below the regular sign in
form. Click the icon to begin the authentication process. Auth0 will ask the form. Click the icon to begin the authentication process. Auth0 will ask the
user to sign in and authorize the GitLab application. If everything goes well user to sign in and authorize the GitLab application. If everything goes well
the user will be returned to GitLab and will be signed in. the user will be returned to GitLab and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -78,6 +78,10 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap ...@@ -78,6 +78,10 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap
1. Save the configuration file. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -58,8 +58,11 @@ To enable the CAS OmniAuth provider you must register your application with your ...@@ -58,8 +58,11 @@ To enable the CAS OmniAuth provider you must register your application with your
1. Save the configuration file. 1. Save the configuration file.
1. Run `gitlab-ctl reconfigure` for the omnibus package. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
1. Restart GitLab for the changes to take effect.
On the sign in page there should now be a CAS tab in the sign in form. On the sign in page there should now be a CAS tab in the sign in form.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -53,6 +53,11 @@ To enable the Crowd OmniAuth provider you must register your application with Cr ...@@ -53,6 +53,11 @@ To enable the Crowd OmniAuth provider you must register your application with Cr
1. Save the configuration file. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Crowd tab in the sign in form.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
On the sign in page there should now be a Crowd tab in the sign in form.
\ No newline at end of file
...@@ -92,6 +92,10 @@ something else descriptive. ...@@ -92,6 +92,10 @@ something else descriptive.
1. Save the configuration file. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Import projects from GitHub and login to your GitLab instance with your GitHub account. Import projects from GitHub and login to your GitLab instance with your GitHub account.
To enable the GitHub OmniAuth provider you must register your application with GitHub. To enable the GitHub OmniAuth provider you must register your application with GitHub.
GitHub will generate an application ID and secret key for you to use. GitHub will generate an application ID and secret key for you to use.
1. Sign in to GitHub. 1. Sign in to GitHub.
...@@ -22,7 +22,7 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -22,7 +22,7 @@ GitHub will generate an application ID and secret key for you to use.
- Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
1. Select "Register application". 1. Select "Register application".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration. Keep this page open as you continue configuration.
![GitHub app](img/github_app.png) ![GitHub app](img/github_app.png)
...@@ -49,7 +49,7 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -49,7 +49,7 @@ GitHub will generate an application ID and secret key for you to use.
For omnibus package: For omnibus package:
For GitHub.com: For GitHub.com:
```ruby ```ruby
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
...@@ -60,9 +60,9 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -60,9 +60,9 @@ GitHub will generate an application ID and secret key for you to use.
} }
] ]
``` ```
For GitHub Enterprise: For GitHub Enterprise:
```ruby ```ruby
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
...@@ -101,10 +101,14 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -101,10 +101,14 @@ GitHub will generate an application ID and secret key for you to use.
1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. 1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7.
1. Save the configuration file and run `sudo gitlab-ctl reconfigure`. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a GitHub icon below the regular sign in form. On the sign in page there should now be a GitHub icon below the regular sign in form.
Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in. If everything goes well the user will be returned to GitLab and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account. Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account.
To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com. To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
GitLab.com will generate an application ID and secret key for you to use. GitLab.com will generate an application ID and secret key for you to use.
1. Sign in to GitLab.com 1. Sign in to GitLab.com
...@@ -26,8 +26,8 @@ GitLab.com will generate an application ID and secret key for you to use. ...@@ -26,8 +26,8 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select "Submit". 1. Select "Submit".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration. Keep this page open as you continue configuration.
![GitLab app](img/gitlab_app.png) ![GitLab app](img/gitlab_app.png)
1. On your GitLab server, open the configuration file. 1. On your GitLab server, open the configuration file.
...@@ -77,8 +77,12 @@ GitLab.com will generate an application ID and secret key for you to use. ...@@ -77,8 +77,12 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Save the configuration file. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a GitLab.com icon below the regular sign in form. On the sign in page there should now be a GitLab.com icon below the regular sign in form.
Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to your GitLab instance and will be signed in. If everything goes well the user will be returned to your GitLab instance and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
...@@ -74,7 +74,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application ...@@ -74,7 +74,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
1. Save the configuration file. 1. Save the configuration file.
1. Restart GitLab for the changes to take effect. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
...@@ -87,3 +88,6 @@ At this point, when users first try to authenticate to your GitLab installation ...@@ -87,3 +88,6 @@ At this point, when users first try to authenticate to your GitLab installation
1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window). 1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window).
1. Scroll down until you find "Product Name". Change the product name to something more descriptive. 1. Scroll down until you find "Product Name". Change the product name to something more descriptive.
1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users. 1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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