Commit d951d637 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'ce/master' into ce-to-ee

parents 9f182d2c 091874da
......@@ -430,7 +430,7 @@ merge request:
1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md)
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. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
......
......@@ -59,20 +59,38 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature Freeze
On the 7th of each month, the stable branches for the upcoming release will
be frozen for major changes. Merge requests may still be merged into master
during this period. By freezing the stable branches prior to a release there's
no need to worry about last minute merge requests potentially breaking a lot of
things.
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.
Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
What is considered to be a major change is determined on a case by case basis as
this definition depends very much on the context of changes. For example, a 5
line change might have a big impact on the entire application. Ultimately the
decision will be made by the maintainers and the release managers.
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
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
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.
## Copy & paste responses
......
......@@ -10,7 +10,6 @@ function requireAll(context) { return context.keys().map(context); }
window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/datepicker');
require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable');
......@@ -36,8 +35,10 @@ require('bootstrap/js/transition');
require('bootstrap/js/tooltip');
require('bootstrap/js/popover');
require('select2/select2.js');
window.Pikaday = require('pikaday');
window._ = require('underscore');
window.Dropzone = require('dropzone');
window.Sortable = require('vendor/Sortable');
require('mousetrap');
require('mousetrap/plugins/pause/mousetrap-pause');
require('./shortcuts');
......
......@@ -6,7 +6,6 @@ function requireAll(context) { return context.keys().map(context); }
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
window.Sortable = require('vendor/Sortable');
requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
......
/* global Vue */
/* global dateFormat */
Vue.filter('due-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');
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
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 */
/* global dateFormat */
/* global Pikaday */
(function(global) {
class DueDateSelect {
......@@ -25,11 +27,14 @@
this.initGlDropdown();
this.initRemoveDueDate();
this.initDatePicker();
this.initStopPropagation();
}
initGlDropdown() {
this.$dropdown.glDropdown({
opened: () => {
const calendar = this.$datePicker.data('pikaday');
calendar.show();
},
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
......@@ -38,25 +43,37 @@
}
initDatePicker() {
this.$datePicker.datepicker({
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']",
onSelect: () => {
const $dueDateInput = $(`input[name='${this.fieldName}']`);
const calendar = new Pikaday({
field: $dueDateInput.get(0),
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')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue();
} else {
return this.saveDueDate(true);
this.saveDueDate(true);
}
}
});
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
}
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
calendar.setDate(null);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
......@@ -67,12 +84,6 @@
});
}
initStopPropagation() {
$(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
return e.stopImmediatePropagation();
});
}
saveDueDate(isDropdown) {
this.parseSelectedDate();
this.prepSelectedDate();
......@@ -86,7 +97,7 @@
// Construct Date object manually to avoid buggy dateString support within Date constructor
const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
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 {
this.displayedDate = 'No due date';
}
......@@ -153,14 +164,24 @@
}
initMilestoneDatePicker() {
$('.datepicker').datepicker({
dateFormat: 'yy-mm-dd'
$('.datepicker').each(function() {
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) => {
e.preventDefault();
const datepicker = $(e.target).siblings('.datepicker');
$.datepicker._clearDate(datepicker);
const calendar = $(e.target).siblings('.datepicker').data('pikaday');
calendar.setDate(null);
});
}
......
......@@ -437,7 +437,7 @@
}
};
GitLabDropdown.prototype.opened = function() {
GitLabDropdown.prototype.opened = function(e) {
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
......@@ -457,6 +457,10 @@
this.positionMenuAbove();
}
if (this.options.opened) {
this.options.opened.call(this, e);
}
return this.dropdown.trigger('shown.gl.dropdown');
};
......
......@@ -3,7 +3,12 @@
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
<<<<<<< HEAD
/* global GroupsSelect */
=======
/* global dateFormat */
/* global Pikaday */
>>>>>>> ce/master
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
......@@ -14,7 +19,7 @@
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
var $issuableDueDate;
var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = bind(this.toggleWip, this);
this.renderWipExplanation = bind(this.renderWipExplanation, this);
......@@ -37,12 +42,14 @@
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
$('.datepicker').datepicker({
dateFormat: 'yy-mm-dd',
onSelect: function(dateText, inst) {
return $issuableDueDate.val(dateText);
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme',
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 */
/* global Flash */
/* global Sortable */
((global) => {
class LabelManager {
......@@ -9,11 +10,12 @@
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
this.prioritizedLabels.sortable({
items: 'li',
placeholder: 'list-placeholder',
axis: 'y',
update: this.onPrioritySortUpdate.bind(this)
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
filter: '.empty-message',
forceFallback: true,
fallbackClass: 'is-dragging',
dataIdAttr: 'data-id',
onUpdate: this.onPrioritySortUpdate.bind(this),
});
this.bindEvents();
}
......@@ -51,13 +53,13 @@
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
if ($from.find('li').length === 1) {
$label.detach().appendTo($target);
if ($from.find('li').length) {
$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');
}
$label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
......@@ -101,8 +103,12 @@
getSortedLabelsIds() {
const sortedIds = [];
this.prioritizedLabels.find('li').each(function() {
sortedIds.push($(this).data('id'));
this.prioritizedLabels.find('> li').each(function() {
const id = $(this).data('id');
if (id) {
sortedIds.push(id);
}
});
return sortedIds;
}
......
/* global Pikaday */
/* global dateFormat */
(() => {
// 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
......@@ -11,21 +13,34 @@
}
const inputs = $(selector);
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
onSelect: function onSelect() {
$(this).trigger('change');
toggleClearInput.call(this);
},
inputs.each((i, el) => {
const $input = $(el);
const calendar = new Pikaday({
field: $input.get(0),
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) {
event.preventDefault();
const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null)
.trigger('change');
const calendar = input.data('pikaday');
calendar.setDate(null);
input.trigger('change');
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 */
/* global Flash */
/* global Sortable */
(function() {
this.Milestone = (function() {
......@@ -8,11 +9,9 @@
type: "PUT",
url: issue_url,
data: data,
success: (function(_this) {
return function(_data) {
return _this.successCallback(_data, li);
};
})(this),
success: function(_data) {
return Milestone.successCallback(_data, li);
},
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
......@@ -27,11 +26,9 @@
type: "PUT",
url: sort_issues_url,
data: data,
success: (function(_this) {
return function(_data) {
return _this.successCallback(_data);
};
})(this),
success: function(_data) {
return Milestone.successCallback(_data);
},
error: function() {
return new Flash("Issues update failed", 'alert');
},
......@@ -46,11 +43,9 @@
type: "PUT",
url: sort_mr_url,
data: data,
success: (function(_this) {
return function(_data) {
return _this.successCallback(_data);
};
})(this),
success: function(_data) {
return Milestone.successCallback(_data);
},
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
......@@ -63,11 +58,9 @@
type: "PUT",
url: merge_request_url,
data: data,
success: (function(_this) {
return function(_data) {
return _this.successCallback(_data, li);
};
})(this),
success: function(_data) {
return Milestone.successCallback(_data, li);
},
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
......@@ -81,65 +74,30 @@
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16');
$(element).find('.assignee-icon').html(img_tag);
$(element).find('.assignee-icon img').replaceWith(img_tag);
} else {
$(element).find('.assignee-icon').html('');
$(element).find('.assignee-icon').empty();
}
return $(element).effect('highlight');
};
function Milestone() {
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.bindMergeRequestSorting();
this.bindTabsSwitching();
}
Milestone.prototype.bindIssuesSorting = function() {
return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
beforeStart: function(event, ui) {
return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
},
stop: function(event, ui) {
return $(".issues-sortable-list").css("min-height", "0px");
},
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();
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, {
group: 'issue-list',
listEls: $('.issues-sortable-list'),
fieldName: 'issue',
sortCallback: Milestone.sortIssues,
updateCallback: Milestone.updateIssue,
});
}.bind(this));
};
Milestone.prototype.bindTabsSwitching = function() {
......@@ -154,42 +112,62 @@
};
Milestone.prototype.bindMergeRequestSorting = function() {
return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
beforeStart: function(event, ui) {
return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, {
group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request',
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) {
return $(".merge_requests-sortable-list").css("min-height", "0px");
onEnd: function () {
opts.listEls.css("min-height", "0px");
},
update: function(event, ui) {
var data;
data = $(this).sortable("serialize");
return Milestone.sortMergeRequests(data);
onUpdate: function(e) {
var ids = this.toArray(),
data;
if (ids.length) {
data = ids.map(function(id) {
return 'sortable_' + opts.fieldName + '[]=' + id;
}).join('&');
opts.sortCallback(data);
}
},
receive: function(event, ui) {
var data, merge_request_id, merge_request_url, new_state;
new_state = $(this).data('state');
merge_request_id = ui.item.data('iid');
merge_request_url = ui.item.data('url');
onAdd: function (e) {
var data, issuableId, issuableUrl, newState;
newState = e.to.dataset.state;
issuableUrl = e.item.dataset.url;
data = (function() {
switch (new_state) {
switch (newState) {
case 'ongoing':
return "merge_request[assignee_id]=" + gon.current_user_id;
return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
case 'unassigned':
return "merge_request[assignee_id]=";
return opts.fieldName + '[assignee_id]=';
case 'closed':
return "merge_request[state_event]=close";
return opts.fieldName + '[state_event]=close';
}
})();
if ($(ui.sender).data('state') === "closed") {
data += "&merge_request[state_event]=reopen";
if (e.from.dataset.state === 'closed') {
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;
......
......@@ -50,14 +50,15 @@
return (
children[target.index] ||
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) {
var rect = el.getBoundingClientRect();
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var height = rect.bottom - rect.top + 10;
return {
x: rect.left,
......
......@@ -111,7 +111,7 @@ require('./commit');
* If provided, returns the commit ref.
* 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.
*
* @returns {Object|Undefined}
......@@ -119,8 +119,8 @@ require('./commit');
commitRef() {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'url') {
accumulator.path = this.pipeline.ref[prop];
if (prop === 'path') {
accumulator.ref_url = this.pipeline.ref[prop];
} else {
accumulator[prop] = this.pipeline.ref[prop];
}
......
......@@ -2,7 +2,6 @@
* 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
* 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.atwho
*= require select2
......@@ -19,6 +18,8 @@
* directory.
*/
@import "../../../node_modules/pikaday/scss/pikaday";
/*
* GitLab UI framework
*/
......
......@@ -43,3 +43,56 @@
float: right;
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 @@
max-height: 230px;
}
.ui-widget {
table {
margin: 0;
}
&.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;
.pika-single {
position: relative!important;
top: 0!important;
border: 0;
box-shadow: none;
}
.ui-datepicker-unselectable {
background-color: $gray-light;
.pika-lendar {
margin-top: -5px;
margin-bottom: 0;
}
}
......
......@@ -2,42 +2,6 @@
font-family: $regular_font;
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 {
border-color: $jq-ui-border;
padding: 0;
......@@ -59,25 +23,4 @@
border: 0;
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 @@
}
.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 {
color: $gl-text-color;
......
......@@ -125,6 +125,12 @@
line-height: 16px;
}
@media (min-width: $screen-sm-min) {
.stage-cell {
padding: 0 4px;
}
}
@media (max-width: $screen-xs-max) {
order: 1;
margin-top: $gl-padding-top;
......
......@@ -178,3 +178,9 @@
}
}
}
.issuable-row {
background-color: $white-light;
cursor: -webkit-grab;
cursor: grab;
}
......@@ -201,10 +201,6 @@
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 {
width: 90%;
......
class Admin::DashboardController < Admin::ApplicationController
def index
@projects = Project.limit(10)
@projects = Project.with_route.limit(10)
@users = User.limit(10)
<<<<<<< HEAD
@groups = Group.limit(10)
@license = License.current
=======
@groups = Group.with_route.limit(10)
>>>>>>> ce/master
end
end
......@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
@groups = Group.with_statistics
@groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
......@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
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."
end
......
class Dashboard::GroupsController < Dashboard::ApplicationController
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
......@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new, :create]
# 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 :user_actions, only: [:show, :subgroups]
layout :determine_layout
def index
......@@ -37,13 +39,6 @@ class GroupsController < Groups::ApplicationController
end
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
respond_to do |format|
......@@ -62,6 +57,11 @@ class GroupsController < Groups::ApplicationController
end
end
def subgroups
@nested_groups = group.children
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
def activity
respond_to do |format|
format.html
......@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
end
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."
end
......@@ -99,13 +99,16 @@ class GroupsController < Groups::ApplicationController
protected
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.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
@shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
end
def authorize_create_group!
......@@ -138,7 +141,8 @@ class GroupsController < Groups::ApplicationController
:public,
:request_access_enabled,
:share_with_group_lock,
:visibility_level
:visibility_level,
:parent_id
]
end
......@@ -154,4 +158,11 @@ class GroupsController < Groups::ApplicationController
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
def user_actions
if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
end
......@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else
object[:error] = {
code: 404,
......
......@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy
if note.editable?
Notes::DeleteService.new(project, current_user).execute(note)
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
......
......@@ -24,7 +24,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
DeleteUserService.new(current_user).execute(current_user)
Users::DestroyService.new(current_user).execute(current_user)
respond_to do |format|
format.html do
......
......@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder
def execute(current_user = nil)
segments = all_groups(current_user)
find_union(segments, Group).order_id_desc
find_union(segments, Group).with_route.order_id_desc
end
private
......
......@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
find_union(segments, Project)
find_union(segments, Project).with_route
end
private
......
......@@ -64,11 +64,11 @@ module MergeRequestsHelper
end
def mr_closes_issues
@mr_closes_issues ||= @merge_request.closes_issues
@mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
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
def mr_change_branches_path(merge_request)
......
......@@ -86,7 +86,9 @@ module TodosHelper
[
{ id: '', text: 'Any Action' },
{ 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
......
......@@ -21,7 +21,7 @@ module Ci
end
serialize :options
serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
......
# 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
extend ActiveSupport::Concern
......@@ -9,7 +9,13 @@ module Routable
validates_associated :route
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
class_methods do
......@@ -77,10 +83,62 @@ module Routable
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
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.path = full_path
route.path = build_full_path
route.name = build_full_name
@full_path = nil
@full_name = nil
end
end
......@@ -569,7 +569,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# 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?
transaction do
......@@ -581,10 +581,6 @@ class MergeRequest < ActiveRecord::Base
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.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
......@@ -598,13 +594,13 @@ class MergeRequest < ActiveRecord::Base
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
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description)
ext.issues - closes_issues
ext.issues - closes_issues(current_user)
end
def target_project_path
......
......@@ -8,6 +8,11 @@ class Namespace < ActiveRecord::Base
include Gitlab::CurrentSettings
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
has_many :projects, dependent: :destroy
......@@ -30,6 +35,8 @@ class Namespace < ActiveRecord::Base
length: { maximum: 255 },
namespace: true
validate :nesting_level_allowed
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
......@@ -171,6 +178,7 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
<<<<<<< HEAD
def actual_size_limit
current_application_settings.repository_size_limit
end
......@@ -183,23 +191,16 @@ class Namespace < ActiveRecord::Base
end
end
=======
>>>>>>> ce/master
def shared_runners_enabled?
projects.with_shared_runners.any?
end
def full_name
@full_name ||=
if parent
parent.full_name + ' / ' + name
else
name
end
end
# Scopes the model on ancestors of the record
def ancestors
if parent_id
path = route.path
path = route ? route.path : full_path
paths = []
until path.blank?
......@@ -222,6 +223,10 @@ class Namespace < ActiveRecord::Base
[owner_id]
end
def parent_changed?
parent_id_changed?
end
private
def repository_storage_paths
......@@ -260,10 +265,6 @@ class Namespace < ActiveRecord::Base
find_each(&:refresh_members_authorized_projects)
end
def full_path_changed?
path_changed? || parent_id_changed?
end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
......@@ -279,4 +280,10 @@ class Namespace < ActiveRecord::Base
path_was
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
......@@ -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_statistics, -> { includes(:statistics) }
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!
scope :with_feature_enabled, ->(feature) {
......@@ -915,26 +920,6 @@ class Project < ActiveRecord::Base
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)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
......@@ -1570,6 +1555,18 @@ class Project < ActiveRecord::Base
map.public_path_for_source_path(path)
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
def cross_namespace_reference?(from)
......@@ -1597,8 +1594,20 @@ class Project < ActiveRecord::Base
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
<<<<<<< HEAD
def full_path_changed?
path_changed? || namespace_id_changed?
=======
# Similar to the normal callbacks that hook into the life cycle of an
# 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
>>>>>>> ce/master
end
def update_project_statistics
......
......@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
after_update :rename_descendants, if: :path_changed?
after_update :rename_descendants
def rename_descendants
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_descendants method
route.update_column(:path, route.path.sub(path_was, path))
if path_changed? || name_changed?
descendants = Route.where('path LIKE ?', "#{path_was}/%")
descendants.each do |route|
attributes = {}
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
# rubocop:enable Rails/FindEach
end
end
......@@ -130,7 +130,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
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 :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
......
class EnvironmentSerializer < BaseSerializer
Item = Struct.new(:name, :size, :latest)
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
class PipelineSerializer < BaseSerializer
class InvalidResourceError < StandardError; end
include API::Helpers::Pagination
Struct.new('Pagination', :request, :response)
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)
tap { @pagination = Struct::Pagination.new(request, response) }
tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
private
# Methods needed by `API::Helpers::Pagination`
#
def params
@pagination.request.query_parameters
def paginated?
@paginator.present?
end
def request
@pagination.request
end
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
resource = resource.includes(project: :namespace)
end
def header(header, value)
@pagination.response.headers[header] = value
if paginated?
super(@paginator.paginate(resource), opts)
else
super(resource, opts)
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
class DeleteService < BaseService
class DestroyService < BaseService
def execute(note)
note.destroy
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"
- 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/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
......@@ -4,38 +4,12 @@
- if current_user
= 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
%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)
= render 'groups/home_panel'
- 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 }
.top-area
%ul.nav-links
%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
= render 'groups/show_nav'
.nav-controls
= 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
......@@ -44,15 +18,4 @@
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
New Project
.tab-content
.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
= render "projects", projects: @projects
- @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 @@
:javascript
var date = $('#personal_access_token_expires_at').val();
var datepicker = $(".datepicker").datepicker({
dateFormat: "yy-mm-dd",
minDate: 0
var $dateField = $('#personal_access_token_expires_at');
var date = $dateField.val();
new Pikaday({
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() {
......
......@@ -5,7 +5,7 @@
- content_for :page_specific_javascripts do
= 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-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
......
......@@ -22,9 +22,7 @@
= 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)
.control
= link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
= icon('plus')
Create Merge Request
= link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
......
......@@ -23,6 +23,4 @@
- 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'
- elsif create_mr_button?
= link_to create_mr_path, class: 'prepend-left-10 btn' do
= icon("plus")
Create Merge Request
= link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn'
- content_for :note_actions do
- 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 '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 '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}, 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
= render 'projects/notes/notes_with_form'
......@@ -35,9 +35,9 @@
= 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)
%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
= 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
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin?
......@@ -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
New 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 '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 '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 }, 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?
= 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'
......
......@@ -3,6 +3,9 @@
- hide_class = ''
= 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?
%div{ class: container_class }
.top-area.adjust
......
......@@ -3,6 +3,9 @@
- page_description @milestone.description
= 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 }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
......
- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
......@@ -11,11 +12,15 @@
.col-sm-10
.input-group.gl-field-error-anchor
.input-group-addon
= root_url
%span>= root_url
- if parent
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
title: 'Please choose a group name with no special characters.'
- if parent
= f.hidden_field :parent_id, value: parent.id
- if @group.persisted?
.alert.alert-warning.prepend-top-10
......
- group_member = local_assigns[:group_member]
- full_name = true unless local_assigns[:full_name] == false
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
......@@ -28,7 +29,10 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
= group.full_name
- if full_name
= group.full_name
- else
= group.name
- if group_member
as
......
......@@ -10,6 +10,3 @@
.col-sm-10
= 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
:javascript
new gl.DueDateSelectors();
......@@ -5,7 +5,7 @@
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
- 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
- if show_project_name
%strong #{project.name} &middot;
......
- @sort ||= sort_value_recently_updated
- personal = params[:personal]
- archived = params[:archived]
- shared = params[:shared]
- namespace_id = params[:namespace_id]
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
......@@ -28,3 +29,14 @@
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
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
delete_user = User.find(delete_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
......@@ -11,6 +11,6 @@ class GroupDestroyWorker
user = User.find(user_id)
DestroyGroupService.new(group, user).execute
Groups::DestroyService.new(group, user).execute
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',
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical
end
......@@ -17,7 +17,7 @@ var config = {
application: './application.js',
blob_edit: './blob_edit/blob_edit_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',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
......
class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
def up
add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false
end
def down
remove_column :protected_branches, :developers_can_merge
end
end
......@@ -14,7 +14,11 @@ class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
disable_ddl_transaction!
def change
def up
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
end
def down
remove_column :spam_logs, :submitted_as_ham
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
t.string "path", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
end
add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
......
......@@ -32,7 +32,8 @@ GET /groups
"web_url": "http://localhost:3000/groups/foo-bar",
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar"
"full_path": "foo-bar",
"parent_id": null
}
]
```
......@@ -156,8 +157,9 @@ Example response:
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar",
"full_name": "Twitter",
"full_path": "twitter",
"parent_id": null,
"projects": [
{
"id": 7,
......@@ -350,6 +352,7 @@ Parameters:
- `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
- `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
......@@ -401,6 +404,7 @@ Example response:
"request_access_enabled": false,
"full_name": "Foobar Group",
"full_path": "foo-bar",
"parent_id": null,
"projects": [
{
"id": 9,
......
......@@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files
```
```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:
......
......@@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE
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
```
......
......@@ -80,10 +80,13 @@ from step 5.
1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
page from step 5.
1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
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 an Auth0 icon below the regular sign in
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
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
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.
[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
1. Save the configuration file.
1. Run `gitlab-ctl reconfigure` for the omnibus package.
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 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
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.
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.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
......@@ -2,7 +2,7 @@
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.
1. Sign in to GitHub.
......@@ -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}'
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.
![GitHub app](img/github_app.png)
......@@ -49,7 +49,7 @@ GitHub will generate an application ID and secret key for you to use.
For omnibus package:
For GitHub.com:
```ruby
gitlab_rails['omniauth_providers'] = [
{
......@@ -60,9 +60,9 @@ GitHub will generate an application ID and secret key for you to use.
}
]
```
For GitHub Enterprise:
```ruby
gitlab_rails['omniauth_providers'] = [
{
......@@ -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. 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.
Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
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.
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 @@
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.
1. Sign in to GitLab.com
......@@ -26,8 +26,8 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select "Submit".
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.
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.
![GitLab app](img/gitlab_app.png)
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.
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.
Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
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.
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
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.
......@@ -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. 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.
[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.
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