Commit 727af20c authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-10-11' into 'master'

CE upstream - Wednesday

Closes gitlab-com/support-forum#2476

See merge request gitlab-org/gitlab-ee!3121
parents bd7ba308 5049eb42
......@@ -588,7 +588,7 @@ karma:
- chrome_debug.log
- coverage-javascript/
codeclimate:
codequality:
<<: *except-docs
<<: *pull-cache
before_script: []
......
......@@ -414,7 +414,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -297,7 +297,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.39.0)
gitaly-proto (0.41.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1062,7 +1062,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.39.0)
gitaly-proto (~> 0.41.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
import { visitUrl } from './lib/utils/url_utility';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
window.BuildArtifacts = (function() {
function BuildArtifacts() {
export default class BuildArtifacts {
constructor() {
this.disablePropagation();
this.setupEntryClick();
this.setupTooltips();
}
BuildArtifacts.prototype.disablePropagation = function() {
$('.top-block').on('click', '.download', function(e) {
// eslint-disable-next-line class-methods-use-this
disablePropagation() {
$('.top-block').on('click', '.download', function (e) {
return e.stopPropagation();
});
return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {
return e.stopImmediatePropagation();
});
};
BuildArtifacts.prototype.setupEntryClick = function() {
return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
}
// eslint-disable-next-line class-methods-use-this
setupEntryClick() {
return $('.tree-holder').on('click', 'tr[data-link]', function () {
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
};
BuildArtifacts.prototype.setupTooltips = function() {
}
// eslint-disable-next-line class-methods-use-this
setupTooltips() {
$('.js-artifact-tree-tooltip').tooltip({
placement: 'bottom',
// Stop the tooltip from hiding when we stop hovering the element directly
......@@ -41,7 +41,5 @@ window.BuildArtifacts = (function() {
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
});
};
return BuildArtifacts;
})();
}
}
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */
/* eslint-disable func-names*/
$(function() {
$('.reveal-variables').off('click').on('click', function() {
$('.js-build-variables').toggle();
$(this).hide();
});
});
export default function handleRevealVariables() {
$('.js-reveal-variables')
.off('click')
.on('click', function () {
$('.js-build-variables').toggle();
$(this).hide();
});
}
window.gl = window.gl || {};
class CILintEditor {
export default class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
......@@ -13,5 +10,3 @@ class CILintEditor {
});
}
}
gl.CILintEditor = CILintEditor;
......@@ -12,7 +12,8 @@
/* global NotificationsDropdown */
/* global GroupAvatar */
/* global LineHighlighter */
/* global BuildArtifacts */
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */
/* global Search */
/* global Admin */
......@@ -29,7 +30,8 @@
/* global ProjectNew */
/* global ProjectShow */
/* global ProjectImport */
/* global Labels */
import Labels from './labels';
import LabelManager from './label_manager';
/* global Shortcuts */
/* global ShortcutsFindFile */
/* global Sidebar */
......@@ -82,6 +84,7 @@ import initChangesDropdown from './init_changes_dropdown';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
import U2FAuthenticate from './u2f/authenticate';
// EE-only
import ApproversSelect from './approvers_select';
......@@ -100,8 +103,8 @@ import initGroupAnalytics from './init_group_analytics';
}
Dispatcher.prototype.initPageScripts = function() {
var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
page = $('body').attr('data-page');
var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
const page = $('body').attr('data-page');
if (!page) {
return false;
}
......@@ -503,7 +506,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'groups:labels:index':
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
new gl.LabelManager();
new LabelManager();
}
$('.label-subscription').each((i, el) => {
const $el = $(el);
......@@ -564,7 +567,7 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'ci:lints:create':
case 'ci:lints:show':
new gl.CILintEditor();
new CILintEditor();
break;
case 'users:show':
new UserCallout();
......@@ -611,14 +614,16 @@ import initGroupAnalytics from './init_group_analytics';
case 'sessions':
case 'omniauth_callbacks':
if (!gon.u2f) break;
gl.u2fAuthenticate = new gl.U2FAuthenticate(
const u2fAuthenticate = new U2FAuthenticate(
$('#js-authenticate-u2f'),
'#js-login-u2f-form',
gon.u2f,
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
gl.u2fAuthenticate.start();
u2fAuthenticate.start();
// needed in rspec
gl.u2fAuthenticate = u2fAuthenticate;
case 'admin':
new Admin();
switch (path[1]) {
......
......@@ -237,9 +237,12 @@ window.DropzoneInput = (function() {
};
const insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
const $child = $(child);
$child.val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
$child.trigger('change');
};
const appendToTextArea = function(url) {
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
import { highCountTrim } from '~/lib/utils/text_utility';
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
/**
* Updates todo counter when todos are toggled.
* When count is 0, we hide the badge.
*
* @param {jQuery.Event} e
* @param {String} count
*/
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
/* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */
import _ from 'underscore';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
window.Build = (function () {
Build.timeout = null;
Build.state = null;
function Build(options) {
export default class Job {
constructor(options) {
this.timeout = null;
this.state = null;
this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl;
......@@ -19,9 +16,7 @@ window.Build = (function () {
this.$document = $(document);
this.logBytes = 0;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -33,7 +28,7 @@ window.Build = (function () {
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(Build.timeout);
clearTimeout(this.timeout);
this.initSidebar();
this.populateJobs(this.buildStage);
......@@ -85,7 +80,7 @@ window.Build = (function () {
this.getBuildTrace();
}
Build.prototype.initAffixTopArea = function () {
initAffixTopArea() {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
......@@ -100,13 +95,14 @@ window.Build = (function () {
top: offsetTop,
},
});
};
}
Build.prototype.canScroll = function () {
// eslint-disable-next-line class-methods-use-this
canScroll() {
return $(document).height() > $(window).height();
};
}
Build.prototype.toggleScroll = function () {
toggleScroll() {
const currentPosition = $(document).scrollTop();
const scrollHeight = $(document).height();
......@@ -119,7 +115,7 @@ window.Build = (function () {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Build Log
// User is at Top of Log
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
......@@ -133,38 +129,40 @@ window.Build = (function () {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
};
}
Build.prototype.scrollDown = function () {
// eslint-disable-next-line class-methods-use-this
scrollDown() {
$(document).scrollTop($(document).height());
};
}
Build.prototype.scrollToBottom = function () {
scrollToBottom() {
this.scrollDown();
this.hasBeenScrolled = true;
this.toggleScroll();
};
}
Build.prototype.scrollToTop = function () {
scrollToTop() {
$(document).scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
};
}
Build.prototype.toggleDisableButton = function ($button, disable) {
// eslint-disable-next-line class-methods-use-this
toggleDisableButton($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
}
Build.prototype.toggleScrollAnimation = function (toggle) {
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
};
}
Build.prototype.initSidebar = function () {
initSidebar() {
this.$sidebar = $('.js-build-sidebar');
};
}
Build.prototype.getBuildTrace = function () {
getBuildTrace() {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
data: { state: this.state },
......@@ -204,7 +202,7 @@ window.Build = (function () {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => {
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
} else {
......@@ -225,14 +223,14 @@ window.Build = (function () {
}
})
.then(() => this.toggleScroll());
};
Build.prototype.shouldHideSidebarForViewport = function () {
}
// eslint-disable-next-line class-methods-use-this
shouldHideSidebarForViewport() {
const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
}
Build.prototype.toggleSidebar = function (shouldHide) {
toggleSidebar(shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
......@@ -249,17 +247,17 @@ window.Build = (function () {
} else {
$toggleButton.removeClass('hidden');
}
};
}
Build.prototype.sidebarOnResize = function () {
sidebarOnResize() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
}
Build.prototype.sidebarOnClick = function () {
sidebarOnClick() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function () {
}
// eslint-disable-next-line class-methods-use-this, consistent-return
updateArtifactRemoveDate() {
const $date = $('.js-artifacts-remove');
if ($date.length) {
const date = $date.text();
......@@ -267,23 +265,21 @@ window.Build = (function () {
gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
);
}
};
Build.prototype.populateJobs = function (stage) {
}
// eslint-disable-next-line class-methods-use-this
populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
};
Build.prototype.updateStageDropdownText = function (stage) {
}
// eslint-disable-next-line class-methods-use-this
updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
};
}
Build.prototype.updateDropdown = function (e) {
updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
};
return Build;
})();
}
}
......@@ -5,7 +5,8 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
import '../build';
import Job from '../job';
import handleRevealVariables from '../build_variables';
export default class JobMediator {
constructor(options = {}) {
......@@ -20,7 +21,8 @@ export default class JobMediator {
}
initBuildClass() {
this.build = new Build();
this.build = new Job();
handleRevealVariables();
}
fetchJob() {
......
......@@ -3,123 +3,119 @@
import Flash from './flash';
((global) => {
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
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.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();
}
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
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.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();
}
bindEvents() {
this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
bindEvents() {
this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
onTogglePriorityClick(e) {
e.preventDefault();
const _this = e.data;
const $btn = $(e.currentTarget);
const $label = $(`#${$btn.data('domId')}`);
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip.tooltip('destroy');
_this.toggleLabelPriority($label, action);
_this.toggleEmptyState($label, $btn, action);
}
onTogglePriorityClick(e) {
e.preventDefault();
const _this = e.data;
const $btn = $(e.currentTarget);
const $label = $(`#${$btn.data('domId')}`);
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip.tooltip('destroy');
_this.toggleLabelPriority($label, action);
_this.toggleEmptyState($label, $btn, action);
}
onButtonActionClick(e) {
e.stopPropagation();
$(e.currentTarget).tooltip('hide');
}
onButtonActionClick(e) {
e.stopPropagation();
$(e.currentTarget).tooltip('hide');
}
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
}
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
}
toggleLabelPriority($label, action, persistState) {
if (persistState == null) {
persistState = true;
}
let xhr;
const _this = this;
const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels;
let $from = this.otherLabels;
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
$label.detach().appendTo($target);
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
if ($target.find('> li:not(.empty-message)').length) {
$target.find('.empty-message').addClass('hidden');
}
// Return if we are not persisting state
if (!persistState) {
return;
}
if (action === 'remove') {
xhr = $.ajax({
url,
type: 'DELETE'
});
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
} else {
xhr = this.savePrioritySort($label, action);
}
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
toggleLabelPriority($label, action, persistState) {
if (persistState == null) {
persistState = true;
}
onPrioritySortUpdate() {
const xhr = this.savePrioritySort();
return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
});
let xhr;
const _this = this;
const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels;
let $from = this.otherLabels;
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
savePrioritySort() {
return $.post({
url: this.prioritizedLabels.data('url'),
data: {
label_ids: this.getSortedLabelsIds()
}
$label.detach().appendTo($target);
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
if ($target.find('> li:not(.empty-message)').length) {
$target.find('.empty-message').addClass('hidden');
}
// Return if we are not persisting state
if (!persistState) {
return;
}
if (action === 'remove') {
xhr = $.ajax({
url,
type: 'DELETE'
});
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
} else {
xhr = this.savePrioritySort($label, action);
}
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
onPrioritySortUpdate() {
const xhr = this.savePrioritySort();
return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
}
});
}
getSortedLabelsIds() {
const sortedIds = [];
this.prioritizedLabels.find('> li').each(function() {
const id = $(this).data('id');
savePrioritySort() {
return $.post({
url: this.prioritizedLabels.data('url'),
data: {
label_ids: this.getSortedLabelsIds()
}
});
}
if (id) {
sortedIds.push(id);
}
});
return sortedIds;
}
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert');
}
gl.LabelManager = LabelManager;
})(window.gl || (window.gl = {}));
getSortedLabelsIds() {
const sortedIds = [];
this.prioritizedLabels.find('> li').each(function() {
const id = $(this).data('id');
if (id) {
sortedIds.push(id);
}
});
return sortedIds;
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */
(function() {
this.Labels = (function() {
function Labels() {
this.setSuggestedColor = this.setSuggestedColor.bind(this);
this.updateColorPreview = this.updateColorPreview.bind(this);
var form;
form = $('.label-form');
this.cleanBinding();
this.addBinding();
this.updateColorPreview();
}
export default class Labels {
constructor() {
this.setSuggestedColor = this.setSuggestedColor.bind(this);
this.updateColorPreview = this.updateColorPreview.bind(this);
this.cleanBinding();
this.addBinding();
this.updateColorPreview();
}
Labels.prototype.addBinding = function() {
$(document).on('click', '.suggest-colors a', this.setSuggestedColor);
return $(document).on('input', 'input#label_color', this.updateColorPreview);
};
addBinding() {
$(document).on('click', '.suggest-colors a', this.setSuggestedColor);
return $(document).on('input', 'input#label_color', this.updateColorPreview);
}
// eslint-disable-next-line class-methods-use-this
cleanBinding() {
$(document).off('click', '.suggest-colors a');
return $(document).off('input', 'input#label_color');
}
// eslint-disable-next-line class-methods-use-this
updateColorPreview() {
const previewColor = $('input#label_color').val();
return $('div.label-color-preview').css('background-color', previewColor);
// Updates the the preview color with the hex-color input
}
Labels.prototype.cleanBinding = function() {
$(document).off('click', '.suggest-colors a');
return $(document).off('input', 'input#label_color');
};
Labels.prototype.updateColorPreview = function() {
var previewColor;
previewColor = $('input#label_color').val();
return $('div.label-color-preview').css('background-color', previewColor);
// Updates the the preview color with the hex-color input
};
// Updates the preview color with a click on a suggested color
Labels.prototype.setSuggestedColor = function(e) {
var color;
color = $(e.currentTarget).data('color');
$('input#label_color').val(color);
this.updateColorPreview();
// Notify the form, that color has changed
$('.label-form').trigger('keyup');
return e.preventDefault();
};
return Labels;
})();
}).call(window);
// Updates the preview color with a click on a suggested color
setSuggestedColor(e) {
const color = $(e.currentTarget).data('color');
$('input#label_color').val(color);
this.updateColorPreview();
// Notify the form, that color has changed
$('.label-form').trigger('keyup');
return e.preventDefault();
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
import 'vendor/latinise';
......@@ -13,9 +13,17 @@ if ((base = w.gl).text == null) {
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
gl.text.highCountTrim = function(count) {
/**
* Returns '99+' for numbers bigger than 99.
*
* @param {Number} count
* @return {Number|String}
*/
export function highCountTrim(count) {
return count > 99 ? '99+' : count;
};
}
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
......
......@@ -46,26 +46,14 @@ import './lib/utils/url_utility';
// behaviors
import './behaviors/';
// u2f
import './u2f/authenticate';
import './u2f/error';
import './u2f/register';
import './u2f/util';
// everything else
import './activities';
import './admin';
import './api';
import './ajax_loading_spinner';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './broadcast_message';
import './build';
import './build_artifacts';
import './build_variables';
import './ci_lint_editor';
import './commits';
import './compare';
import './compare_autocomplete';
......@@ -91,8 +79,6 @@ import './issuable_context';
import './issuable_form';
import './issue';
import './issue_status_select';
import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
......@@ -130,7 +116,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
import './star';
import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
import Flash from './flash';
import { __, s__ } from './locale';
export default class Star {
constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
var $starIcon, $starSpan, $this, toggleStar;
$this = $(this);
$starSpan = $this.find('span');
$starIcon = $this.find('i');
toggleStar = function(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
$starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
$('.project-home-panel .toggle-star')
.on('ajax:success', function handleSuccess(e, data) {
const $this = $(this);
const $starSpan = $this.find('span');
const $starIcon = $this.find('i');
function toggleStar(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
$starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
}
};
toggleStar($starSpan.hasClass('starred'));
}).on('ajax:error', function(e, xhr, status, error) {
new Flash('Star toggle failed. Try again later.', 'alert');
});
toggleStar($starSpan.hasClass('starred'));
})
.on('ajax:error', () => {
Flash('Star toggle failed. Try again later.', 'alert');
});
}
}
/* global U2FRegister */
import U2FRegister from './u2f/register';
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
/* eslint-disable func-names, wrap-iife */
/* global u2f */
/* global U2FError */
/* global U2FUtil */
import _ from 'underscore';
import isU2FSupported from './util';
import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
const global = window.gl || (window.gl = {});
global.U2FAuthenticate = (function() {
function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this);
this.renderTemplate = this.renderTemplate.bind(this);
this.authenticate = this.authenticate.bind(this);
this.start = this.start.bind(this);
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
this.form = form;
this.fallbackButton = fallbackButton;
this.fallbackUI = fallbackUI;
if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
this.signRequests = u2fParams.sign_requests.map(function(request) {
// The U2F Javascript API v1.1 requires a single challenge, with
// _no challenges per-request_. The U2F Javascript API v1.0 requires a
// challenge per-request, which is done by copying the single challenge
// into every request.
//
// In either case, we don't need the per-request challenges that the server
// has generated, so we can remove them.
//
// Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
// This can be removed once we upgrade.
// https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
return _(request).omit('challenge');
});
export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this);
this.renderTemplate = this.renderTemplate.bind(this);
this.authenticate = this.authenticate.bind(this);
this.start = this.start.bind(this);
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
this.form = form;
this.fallbackButton = fallbackButton;
this.fallbackUI = fallbackUI;
if (this.fallbackButton) {
this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
}
U2FAuthenticate.prototype.start = function() {
if (U2FUtil.isU2FSupported()) {
return this.renderInProgress();
} else {
return this.renderNotSupported();
}
};
// The U2F Javascript API v1.1 requires a single challenge, with
// _no challenges per-request_. The U2F Javascript API v1.0 requires a
// challenge per-request, which is done by copying the single challenge
// into every request.
//
// In either case, we don't need the per-request challenges that the server
// has generated, so we can remove them.
//
// Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
// This can be removed once we upgrade.
// https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
U2FAuthenticate.prototype.authenticate = function() {
return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) {
return function(response) {
var error;
if (response.errorCode) {
error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
} else {
return _this.renderAuthenticated(JSON.stringify(response));
}
};
})(this), 10);
this.templates = {
notSupported: '#js-authenticate-u2f-not-supported',
setup: '#js-authenticate-u2f-setup',
inProgress: '#js-authenticate-u2f-in-progress',
error: '#js-authenticate-u2f-error',
authenticated: '#js-authenticate-u2f-authenticated',
};
}
// Rendering #
U2FAuthenticate.prototype.templates = {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
"inProgress": '#js-authenticate-u2f-in-progress',
"error": '#js-authenticate-u2f-error',
"authenticated": '#js-authenticate-u2f-authenticated'
};
start() {
if (isU2FSupported()) {
return this.renderInProgress();
}
return this.renderNotSupported();
}
U2FAuthenticate.prototype.renderTemplate = function(name, params) {
var template, templateString;
templateString = $(this.templates[name]).html();
template = _.template(templateString);
return this.container.html(template(params));
};
authenticate() {
return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
return function (response) {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
}
return _this.renderAuthenticated(JSON.stringify(response));
};
})(this), 10);
}
U2FAuthenticate.prototype.renderInProgress = function() {
this.renderTemplate('inProgress');
return this.authenticate();
};
renderTemplate(name, params) {
const templateString = $(this.templates[name]).html();
const template = _.template(templateString);
return this.container.html(template(params));
}
U2FAuthenticate.prototype.renderError = function(error) {
this.renderTemplate('error', {
error_message: error.message(),
error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
};
renderInProgress() {
this.renderTemplate('inProgress');
return this.authenticate();
}
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated');
const container = this.container[0];
container.querySelector('#js-device-response').value = deviceResponse;
container.querySelector(this.form).submit();
this.fallbackButton.classList.add('hidden');
};
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
error_code: error.errorCode,
});
return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
}
U2FAuthenticate.prototype.renderNotSupported = function() {
return this.renderTemplate('notSupported');
};
renderAuthenticated(deviceResponse) {
this.renderTemplate('authenticated');
const container = this.container[0];
container.querySelector('#js-device-response').value = deviceResponse;
container.querySelector(this.form).submit();
this.fallbackButton.classList.add('hidden');
}
U2FAuthenticate.prototype.switchToFallbackUI = function() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
this.fallbackUI.classList.remove('hidden');
};
renderNotSupported() {
return this.renderTemplate('notSupported');
}
switchToFallbackUI() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
this.fallbackUI.classList.remove('hidden');
}
return U2FAuthenticate;
})();
})();
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
/* global u2f */
export default class U2FError {
constructor(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = this.message.bind(this);
this.httpsDisabled = window.location.protocol !== 'https:';
this.u2fFlowType = u2fFlowType;
}
(function() {
this.U2FError = (function() {
function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = this.message.bind(this);
this.httpsDisabled = window.location.protocol !== 'https:';
this.u2fFlowType = u2fFlowType;
}
U2FError.prototype.message = function() {
if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
} else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
message() {
if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
} else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
if (this.u2fFlowType === 'authenticate') {
return 'This device has not been registered with us.';
}
return "There was a problem communicating with your device.";
};
return U2FError;
})();
}).call(window);
if (this.u2fFlowType === 'register') {
return 'This device has already been registered with us.';
}
}
return 'There was a problem communicating with your device.';
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
/* eslint-disable func-names, wrap-iife */
/* global u2f */
/* global U2FError */
/* global U2FUtil */
import _ from 'underscore';
import isU2FSupported from './util';
import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
this.U2FRegister = (function() {
function U2FRegister(container, u2fParams) {
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this);
this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this);
this.renderSetup = this.renderSetup.bind(this);
this.renderTemplate = this.renderTemplate.bind(this);
this.register = this.register.bind(this);
this.start = this.start.bind(this);
this.appId = u2fParams.app_id;
this.registerRequests = u2fParams.register_requests;
this.signRequests = u2fParams.sign_requests;
}
export default class U2FRegister {
constructor(container, u2fParams) {
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this);
this.renderError = this.renderError.bind(this);
this.renderInProgress = this.renderInProgress.bind(this);
this.renderSetup = this.renderSetup.bind(this);
this.renderTemplate = this.renderTemplate.bind(this);
this.register = this.register.bind(this);
this.start = this.start.bind(this);
this.appId = u2fParams.app_id;
this.registerRequests = u2fParams.register_requests;
this.signRequests = u2fParams.sign_requests;
U2FRegister.prototype.start = function() {
if (U2FUtil.isU2FSupported()) {
return this.renderSetup();
} else {
return this.renderNotSupported();
}
this.templates = {
notSupported: '#js-register-u2f-not-supported',
setup: '#js-register-u2f-setup',
inProgress: '#js-register-u2f-in-progress',
error: '#js-register-u2f-error',
registered: '#js-register-u2f-registered',
};
}
U2FRegister.prototype.register = function() {
return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) {
return function(response) {
var error;
if (response.errorCode) {
error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
} else {
return _this.renderRegistered(JSON.stringify(response));
}
};
})(this), 10);
};
start() {
if (isU2FSupported()) {
return this.renderSetup();
}
return this.renderNotSupported();
}
// Rendering #
U2FRegister.prototype.templates = {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
"inProgress": '#js-register-u2f-in-progress',
"error": '#js-register-u2f-error',
"registered": '#js-register-u2f-registered'
};
register() {
return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
return function (response) {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
}
return _this.renderRegistered(JSON.stringify(response));
};
})(this), 10);
}
U2FRegister.prototype.renderTemplate = function(name, params) {
var template, templateString;
templateString = $(this.templates[name]).html();
template = _.template(templateString);
return this.container.html(template(params));
};
renderTemplate(name, params) {
const templateString = $(this.templates[name]).html();
const template = _.template(templateString);
return this.container.html(template(params));
}
U2FRegister.prototype.renderSetup = function() {
this.renderTemplate('setup');
return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
};
renderSetup() {
this.renderTemplate('setup');
return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
}
U2FRegister.prototype.renderInProgress = function() {
this.renderTemplate('inProgress');
return this.register();
};
renderInProgress() {
this.renderTemplate('inProgress');
return this.register();
}
U2FRegister.prototype.renderError = function(error) {
this.renderTemplate('error', {
error_message: error.message(),
error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
renderError(error) {
this.renderTemplate('error', {
error_message: error.message(),
error_code: error.errorCode,
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
}
U2FRegister.prototype.renderRegistered = function(deviceResponse) {
this.renderTemplate('registered');
// Prefer to do this instead of interpolating using Underscore templates
// because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
U2FRegister.prototype.renderNotSupported = function() {
return this.renderTemplate('notSupported');
};
renderRegistered(deviceResponse) {
this.renderTemplate('registered');
// Prefer to do this instead of interpolating using Underscore templates
// because of JSON escaping issues.
return this.container.find('#js-device-response').val(deviceResponse);
}
return U2FRegister;
})();
}).call(window);
renderNotSupported() {
return this.renderTemplate('notSupported');
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
U2FUtil.isU2FSupported = function() {
return window.u2f;
};
return U2FUtil;
})();
}).call(window);
export default function isU2FSupported() {
return window.u2f;
}
......@@ -749,7 +749,7 @@
margin-bottom: $dropdown-vertical-offset;
}
li:not(.dropdown-bold-header) {
li {
display: block;
padding: 0 1px;
......
......@@ -161,9 +161,10 @@
}
}
.dropdown-bold-header {
li.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
padding: 0 16px;
}
.navbar-collapse {
......
......@@ -54,12 +54,15 @@
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
margin-right: 4px;
.stage-cell .stage-container {
margin: 3px 3px 3px 0;
}
.stage-container:last-child {
margin-right: 0;
}
.dropdown-menu {
margin-top: 11px;
}
......
......@@ -236,9 +236,11 @@
}
.stage-cell {
@media (min-width: $screen-md-min) {
min-width: 148px;
margin-right: -4px;
&.table-section {
@media (min-width: $screen-md-min) {
min-width: 148px;
margin-right: -4px;
}
}
.mini-pipeline-graph-dropdown-toggle svg {
......
......@@ -653,6 +653,10 @@ a.deploy-project-label {
}
.project-import {
.form-group {
margin-bottom: 0;
}
.import-btn-container {
margin-bottom: 0;
}
......
......@@ -2,7 +2,8 @@ class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: :index
def index
@runners = Ci::Runner.order('id DESC')
sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc }
@runners = Ci::Runner.order(sort)
@runners = @runners.search(params[:search]) if params[:search].present?
@runners = @runners.page(params[:page]).per(30)
@active_runners_cnt = Ci::Runner.online.count
......
module PreviewMarkdown
extend ActiveSupport::Concern
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
markdown_params =
case controller_name
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
else {}
end
render json: {
body: view_context.markdown(result[:text], markdown_params),
references: {
users: result[:users],
commands: view_context.markdown(result[:commands])
}
}
end
end
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
@sort = params[:sort] || 'id_desc'
@sort = params[:sort] || 'created_desc'
@groups =
if params[:parent_id] && Group.supports_nested_groups?
......
......@@ -2,6 +2,7 @@ class GroupsController < Groups::ApplicationController
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
include PreviewMarkdown
respond_to :html
......
class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
......@@ -93,17 +95,6 @@ class Projects::WikisController < Projects::ApplicationController
def git_access
end
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: result[:users]
}
}
end
private
def load_project_wiki
......
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
prepend EE::ProjectsController
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
......@@ -261,18 +262,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text]),
references: {
users: result[:users],
commands: view_context.markdown(result[:commands])
}
}
end
private
# Render project landing depending of which features are available
......
......@@ -4,6 +4,7 @@ class SnippetsController < ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
include PreviewMarkdown
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
......@@ -87,17 +88,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path, status: 302
end
def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text], skip_project_check: true),
references: {
users: result[:users]
}
}
end
protected
def snippet
......
......@@ -4,8 +4,8 @@ module CompareHelper
to.present? &&
from != to &&
can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
project.repository.branch_exists?(from) &&
project.repository.branch_exists?(to)
end
def create_mr_path(from = params[:from], to = params[:to], project = @project)
......
......@@ -7,7 +7,12 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
def group_icon(group)
def group_icon(group, options = {})
img_path = group_icon_url(group, options)
image_tag img_path, options
end
def group_icon_url(group, options = {})
if group.is_a?(String)
group = Group.find_by_full_path(group)
end
......@@ -95,7 +100,7 @@ module GroupsHelper
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
group_icon(group, class: "avatar-tile", width: 15, height: 15)
else
""
end
......
......@@ -10,6 +10,7 @@ module LazyImageTagHelper
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
options[:class] ||= ""
options[:class] << " lazy"
......
......@@ -11,7 +11,7 @@ module Avatarable
# If asset_host is set then it is expected that assets are handled by a standalone host.
# That means we do not want to get GitLab's relative_url_root option anymore.
host = asset_host.present? ? asset_host : gitlab_host
host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host
[host, avatar.url].join
end
......
......@@ -266,23 +266,22 @@ module Issuable
participants(user).include?(user)
end
def to_hook_data(user)
hook_data = {
object_kind: self.class.name.underscore,
user: user.hook_attrs,
project: project.hook_attrs,
object_attributes: hook_attrs,
labels: labels.map(&:hook_attrs),
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
if self.is_a?(Issue)
hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
else
hook_data[:assignee] = assignee.hook_attrs if assignee
def to_hook_data(user, old_labels: [], old_assignees: [])
changes = previous_changes
if old_labels != labels
changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
end
if old_assignees != assignees
if self.is_a?(Issue)
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
else
changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
end
end
hook_data
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
def labels_array
......
......@@ -86,20 +86,6 @@ class Issue < ActiveRecord::Base
end
end
def hook_attrs
assignee_ids = self.assignee_ids
attrs = {
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate,
assignee_ids: assignee_ids,
assignee_id: assignee_ids.first # This key is deprecated
}
attributes.merge!(attrs)
end
def self.reference_prefix
'#'
end
......@@ -146,6 +132,10 @@ class Issue < ActiveRecord::Base
"id DESC")
end
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
......
......@@ -183,6 +183,10 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
def hook_attrs
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
......@@ -612,24 +616,6 @@ class MergeRequest < ActiveRecord::Base
!discussions_to_be_resolved?
end
def hook_attrs
attrs = {
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
work_in_progress: work_in_progress?,
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate
}
if diff_head_commit
attrs[:last_commit] = diff_head_commit.hook_attrs
end
attributes.merge!(attrs)
end
def for_fork?
target_project != source_project
end
......@@ -714,13 +700,13 @@ class MergeRequest < ActiveRecord::Base
def source_branch_exists?
return false unless self.source_project
self.source_project.repository.branch_names.include?(self.source_branch)
self.source_project.repository.branch_exists?(self.source_branch)
end
def target_branch_exists?
return false unless self.target_project
self.target_project.repository.branch_names.include?(self.target_branch)
self.target_project.repository.branch_exists?(self.target_branch)
end
def merge_commit_message(include_description: false)
......
......@@ -53,13 +53,17 @@ class SentNotification < ActiveRecord::Base
end
def unsubscribable?
!for_commit?
!(for_commit? || for_snippet?)
end
def for_commit?
noteable_type == "Commit"
end
def for_snippet?
noteable_type.end_with?('Snippet')
end
def noteable
if for_commit?
project.commit(commit_id) rescue nil
......
......@@ -45,6 +45,6 @@ class GroupEntity < Grape::Entity
end
expose :avatar_url do |group|
group_icon(group)
group_icon_url(group)
end
end
......@@ -257,7 +257,7 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees)
issuable.update_project_counter_caches if update_project_counters
end
......
module Issues
class BaseService < ::IssuableBaseService
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
issue_data[:object_attributes].merge!(url: issue_url, action: action)
issue_data
def hook_data(issue, action, old_labels: [], old_assignees: [])
hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
hook_data[:object_attributes][:action] = action
hook_data
end
def reopen_service
......@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [])
issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
......
......@@ -20,19 +20,19 @@ module MergeRequests
super if changed_title
end
def hook_data(merge_request, action, oldrev = nil)
hook_data = merge_request.to_hook_data(current_user)
hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [])
hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
hook_data[:object_attributes][:action] = action
if oldrev && !Gitlab::Git.blank_ref?(oldrev)
hook_data[:object_attributes][:oldrev] = oldrev
if old_rev && !Gitlab::Git.blank_ref?(old_rev)
hook_data[:object_attributes][:oldrev] = old_rev
end
hook_data
end
def execute_hooks(merge_request, action = 'open', oldrev = nil)
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [])
if merge_request.project
merge_data = hook_data(merge_request, action, oldrev)
merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
......
......@@ -183,7 +183,7 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
execute_hooks(merge_request, 'update', @oldrev)
execute_hooks(merge_request, 'update', old_rev: @oldrev)
end
end
......
......@@ -459,7 +459,7 @@ module QuickActions
target_branch_param.strip
end
command :target_branch do |branch_name|
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
@updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
desc 'Move issue from one column of the board to another'
......
......@@ -647,7 +647,7 @@ module SystemNoteService
def discussion_lock(issuable, author)
action = issuable.discussion_locked? ? 'locked' : 'unlocked'
body = "#{action} this issue"
body = "#{action} this #{issuable.class.to_s.titleize.downcase}"
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
......
......@@ -22,7 +22,7 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
= group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to [:admin, group], class: 'group-name' do
= group.full_name
......
......@@ -16,7 +16,7 @@
%ul.well-list
%li
.avatar-container.s60
= image_tag group_icon(@group), class: "avatar s60"
= group_icon(@group, class: "avatar s60")
%li
%span.light Name:
%strong= @group.name
......
......@@ -63,7 +63,7 @@
%th Projects
%th Jobs
%th Tags
%th Last contact
%th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
......
......@@ -7,4 +7,4 @@
%td.notes_line{ colspan: 2 }
%td.notes_content
.content{ class: ('hide' unless expanded) }
= render partial: "discussions/notes", collection: discussions, as: :discussion
= render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true }
......@@ -24,4 +24,4 @@
= render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }
.note-container
= render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse: true }
= render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true }
- disable_collapse = local_assigns.fetch(:disable_collapse, false)
- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse
- disable_collapse_class = local_assigns.fetch(:disable_collapse_class, false)
- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse_class
- badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter]
- show_toggle = local_assigns.fetch(:show_toggle, true)
- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
......
......@@ -19,8 +19,7 @@
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
%span ... and #{event.commits_count - 2} more commits.
%span ... and #{pluralize(event.commits_count - 1, 'more commit')}.
- if event.md_ref?
- from = event.commit_from
......
.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
= group_icon(@group, class: "avatar s70 avatar-tile")
%h1.group-title
= @group.name
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
......
......@@ -12,7 +12,7 @@
.form-group
.col-sm-offset-2.col-sm-10
.avatar-container.s160
= image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change your group avatar here
......
......@@ -10,7 +10,7 @@
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: '' } do
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix
.error-alert
......
......@@ -10,7 +10,7 @@
.context-header
= link_to group_path(@group), title: @group.name do
.avatar-container.s40.group-avatar
= image_tag group_icon(@group), class: "avatar s40 avatar-tile"
= group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
%ul.sidebar-top-level-items
......
......@@ -109,7 +109,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project.
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
.settings-content.no-animate{ class: ('expanded' if expanded) }
.sub-section
%h4 Housekeeping
......
......@@ -48,7 +48,7 @@
- if @build.trigger_variables.any?
%p
%button.btn.group.btn-group-justified.reveal-variables Reveal Variables
%button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
......
- page_title _("Wiki")
%h3.page-title= _("Wiki|Empty page")
%h3.page-title= s_("Wiki|Empty page")
%hr
.error_message
= s_("WikiEmptyPageError|You are not allowed to create wiki pages")
......@@ -28,7 +28,7 @@
.avatar-container.s40
= link_to group do
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
= group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to group_name, group, class: 'group-name'
......
......@@ -4,7 +4,7 @@
- dom_id = "group_member_#{group_link.id}"
%li.member.group_member{ id: dom_id }
%span.list-item-name
= image_tag group_icon(group), class: "avatar s40", alt: ''
= group_icon(group, class: "avatar s40", alt: '')
%strong
= link_to group.full_name, group_path(group)
.cgray
......
......@@ -2,4 +2,4 @@
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
.avatar-container.s40
= image_tag group_icon(group), class: 'avatar group-avatar s40'
= group_icon(group, class: 'avatar group-avatar s40')
---
title: Don't show an "Unsubscribe" link in snippet comment notifications
merge_request: 14764
author:
type: fixed
---
title: Include the changes in issuable webhook payloads
merge_request: 14308
author:
type: added
---
title: Fix bad type checking to prevent 0 count badge to be shown
merge_request:
author:
type: fixed
---
title: Fix the project import with issues and milestones
merge_request: 14657
author:
type: fixed
---
title: Decreases z-index of select2 to a lower number of our navigation bar
merge_request:
author:
type: fixed
---
title: Fix the number representing the amount of commits related to a push event
merge_request:
author:
type: fixed
---
title: Fixes mini pipeline graph in commit view
merge_request:
author:
type: fixed
---
title: Add sort runners on admin runners
merge_request: 14661
author: Takuya Noguchi
type: added
---
title: Avoid fetching all branches for branch existence checks
merge_request: 14778
author:
type: changed
---
title: Fixed group sort dropdown defaulting to empty
merge_request:
author:
type: fixed
---
title: Add support for markdown preview to group milestones
merge_request: 14806
author: Vitaliy @blackst0ne Klachkov
type: fixed
namespace :google_api do
resource :auth, only: [], controller: :authorizations do
match :callback, via: [:get, :post]
scope '-' do
namespace :google_api do
resource :auth, only: [], controller: :authorizations do
match :callback, via: [:get, :post]
end
end
end
require 'constraints/group_url_constrainer'
resources :groups, only: [:index, :new, :create]
resources :groups, only: [:index, :new, :create] do
post :preview_markdown
end
constraints(GroupUrlConstrainer.new) do
scope(path: 'groups/*id',
......
# rubocop:disable Migration/Datetime
class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_expire_at, :timestamp
......
# rubocop:disable Migration/Datetime
class AddQueuedAtToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/Datetime
class AddSharedRunnersSecondsToProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
......
# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
......
......@@ -57,18 +57,15 @@
- [Merge Request checklist](database_merge_request_checklist.md)
- [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign keys & associations](foreign_keys.md)
- [Serializing data](serializing_data.md)
- [Polymorphic associations](polymorphic_associations.md)
- [Single table inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md)
- [Polymorphic associations](polymorphic_associations.md)
- [Serializing data](serializing_data.md)
- [Hash indexes](hash_indexes.md)
- [Storing SHA1 hashes as binary](sha1_as_binary.md)
- [Iterating tables in batches](iterating_tables_in_batches.md)
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Hash indexes](hash_indexes.md)
- [Swapping Tables](swapping_tables.md)
## Testing guides
......
......@@ -88,16 +88,31 @@ followed by any global declarations, then a blank newline prior to any imports o
1. Use ES module syntax to import modules
```javascript
// bad
require('foo');
const SomeClass = require('some_class');
// good
import Foo from 'foo';
import SomeClass from 'some_class';
// bad
module.exports = Foo;
module.exports = SomeClass;
// good
export default Foo;
export default SomeClass;
```
Import statements are following usual naming guidelines, for example object literals use camel case:
```javascript
// some_object file
export default {
key: 'value',
};
// bad
import ObjectLiteral from 'some_object';
// good
import objectLiteral from 'some_object';
```
1. Relative paths: when importing a module in the same directory, a child
......@@ -285,6 +300,13 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. **Extensions**: Use `.vue` extension for Vue components.
1. **Reference Naming**: Use camelCase for their instances:
```javascript
// bad
import CardBoard from 'cardBoard'
components: {
CardBoard:
};
// good
import cardBoard from 'cardBoard'
......
......@@ -67,7 +67,7 @@ run JavaScript tests, so you can either run unit tests (e.g. test a single
JavaScript method), or integration tests (e.g. test a component that is composed
of multiple components).
## System tests or Feature tests
## System tests or feature tests
Formal definition: https://en.wikipedia.org/wiki/System_testing.
......@@ -108,7 +108,7 @@ The reasons why we should follow these best practices are as follows:
[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
[RackTest]: https://github.com/teamcapybara/capybara#racktest
## Black-box tests or End-to-end tests
## Black-box tests or end-to-end tests
GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
......
# GitLab Helm Chart
> **Note**:
* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). For more information on available charts, please see our [overview](index.md#chart-overview).
* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
......@@ -8,7 +8,9 @@ For more information on available GitLab Helm Charts, please see our [overview](
## Introduction
The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment.
The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. We **strongly recommend** the [gitlab-omnibus](gitlab_omnibus.md) chart.
This chart is deprecated, and will be replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
This chart includes the following:
......
# GitLab-Omnibus Helm Chart
> **Note:**
* This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on.
* GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these.
* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
......@@ -12,6 +11,8 @@ For more information on available GitLab Helm Charts, please see our [overview](
This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/).
This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) once available. Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
The deployment includes:
- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus
......@@ -23,9 +24,8 @@ The deployment includes:
### Limitations
* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported.
* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available.
* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018.
* This chart is in beta, and suited for small to medium size deployments. [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) are not supported.
* A new generation [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development, and will deprecate this chart. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We plan to release the new chart in beta by the end of 2017.
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
......
......@@ -9,11 +9,11 @@ should be deployed, upgraded, and configured.
## Chart Overview
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added.
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small to medium deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
* [Advanced GitLab Installation](gitlab_chart.md): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options.
* [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
......@@ -21,13 +21,13 @@ should be deployed, upgraded, and configured.
This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
Once the [cloud native GitLab chart](#cloud-native-gitlab-chart) is ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md)
Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
## Cloud Native GitLab Chart
GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
By offering individual containers and charts, we will be able to provide a number of benefits:
* Easier horizontal scaling of each service,
......@@ -35,9 +35,9 @@ By offering individual containers and charts, we will be able to provide a numbe
* Potential for rolling updates and canaries within a service,
* and plenty more.
This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018.
This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017.
Learn more about the [cloud native GitLab chart.](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)
Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
## Other Charts
......
......@@ -194,38 +194,67 @@ load and will have a corresponding badge counter to match the counter on the ima
> [Introduced][ce-14531] in GitLab 10.1.
There might be some cases where a discussion is better off if it's locked down.
For example:
Sometimes a discussion is revolved around an image. With image discussions,
you can easily target a specific coordinate of an image and start a discussion
around it. Image discussions are available in merge requests and commit detail views.
To start an image discussion, hover your mouse over the image. Your mouse pointer
should convert into an icon, indicating that the image is available for commenting.
Simply click anywhere on the image to create a new discussion.
![Start image discussion](img/start_image_discussion.gif)
After you click on the image, a comment form will be displayed that would be the start
of your discussion. Once you save your comment, you will see a new badge displayed on
top of your image. This badge represents your discussion.
>**Note:**
This discussion badge is typically associated with a number that is only used as a visual
reference for each discussion. In the merge request discussion tab,
this badge will be indicated with a comment icon since each discussion will render a new
image section.
Image discussions also work on diffs that replace an existing image. In this diff view
mode, you can toggle the different view modes and still see the discussion point badges.
| 2-up | Swipe | Onion Skin |
| :-----------: | :----------: | :----------: |
| ![2-up view](img/two_up_view.png) | ![swipe view](img/swipe_view.png) | ![onion skin view](img/onion_skin_view.png) |
- Discussions that are several years old and the issue/merge request is closed,
but people continue to try to resurrect the discussion.
- Discussions where someone or a group of people are trolling, are abusive, or
in-general are causing the discussion to be unproductive.
Image discussions also work well with resolvable discussions. Resolved discussions
on diffs (not on the merge request discussion tab) will appear collapsed on page
load and will have a corresponding badge counter to match the counter on the image.
![Image resolved discussion](img/image_resolved_discussion.png)
## Lock discussions
> [Introduced][ce-14531] in GitLab 10.1.
In locked discussions, only team members can write new comments and edit the old
ones.
For large projects with many contributors, it may be useful to stop discussions
in issues or merge requests in these scenarios:
To lock or unlock a discussion, you need to have at least Master [permissions]:
- The project maintainer has already resolved the discussion and it is not helpful
for continued feedback. The project maintainer has already directed new conversation
to newer issues or merge requests.
- The people participating in the discussion are trolling, abusive, or otherwise
being unproductive.
1. Find the "Lock" section in the sidebar and click **Edit**
1. In the dialog that will appear, you can choose to turn on or turn off the
discussion lock
1. Optionally, leave a comment to explain your reasoning behind that action
In these cases, a user with Master permissions or higher in the project can lock (and unlock)
an issue or a merge request, using the "Lock" section in the sidebar:
| Turn off discussion lock | Turn on discussion lock |
| Unlock | Lock |
| :-----------: | :----------: |
| ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) |
Every change is indicated by a system note in the issue's or merge request's
comments.
System notes indicate locking and unlocking.
![Discussion lock system notes](img/discussion_lock_system_notes.png)
Once an issue or merge request is locked, project members can see the indicator
in the comment area, whereas non project members can only see the information
that the discussion is locked.
In a locked issue or merge request, only team members can add new comments and
edit existing comments. Non-team members are restricted from adding or editing comments.
| Team member | Not a member |
| Team member | Non-team member |
| :-----------: | :----------: |
| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
......
......@@ -26,7 +26,7 @@ The following table depicts the various user permission levels in a project.
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
| Lock comments | | | | ✓ | ✓ |
| Lock discussions (issues and merge requests) | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
......@@ -56,7 +56,7 @@ The following table depicts the various user permission levels in a project.
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ |
......@@ -146,6 +146,7 @@ group.
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
### Subgroup permissions
......
......@@ -230,7 +230,7 @@ X-Gitlab-Event: Issue Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project":{
"project": {
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
......@@ -246,7 +246,7 @@ X-Gitlab-Event: Issue Hook
"ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
"http_url":"http://example.com/gitlabhq/gitlab-test.git"
},
"repository":{
"repository": {
"name": "Gitlab Test",
"url": "http://example.com/gitlabhq/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
......@@ -291,7 +291,37 @@ X-Gitlab-Event: Issue Hook
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
}]
}],
"changes": {
"updated_by_id": [null, 1],
"updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
"labels": {
"previous": [{
"id": 206,
"title": "API",
"color": "#ffffff",
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
"updated_at": "2013-12-03T17:15:43Z",
"template": false,
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
}],
"current": [{
"id": 205,
"title": "Platform",
"color": "#123123",
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
"updated_at": "2013-12-03T17:15:43Z",
"template": false,
"description": "Platform related issues",
"type": "ProjectLabel",
"group_id": 41
}]
}
}
}
```
......@@ -686,6 +716,28 @@ X-Gitlab-Event: Merge Request Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"project": {
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
"avatar_url":null,
"git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
"git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
"namespace":"GitlabHQ",
"visibility_level":20,
"path_with_namespace":"gitlabhq/gitlab-test",
"default_branch":"master",
"homepage":"http://example.com/gitlabhq/gitlab-test",
"url":"http://example.com/gitlabhq/gitlab-test.git",
"ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
"http_url":"http://example.com/gitlabhq/gitlab-test.git"
},
"repository": {
"name": "Gitlab Test",
"url": "http://example.com/gitlabhq/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
"homepage": "http://example.com/gitlabhq/gitlab-test"
},
"object_attributes": {
"id": 99,
"target_branch": "master",
......@@ -704,7 +756,7 @@ X-Gitlab-Event: Merge Request Hook
"target_project_id": 14,
"iid": 1,
"description": "",
"source":{
"source": {
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
......@@ -754,6 +806,48 @@ X-Gitlab-Event: Merge Request Hook
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
},
"labels": [{
"id": 206,
"title": "API",
"color": "#ffffff",
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
"updated_at": "2013-12-03T17:15:43Z",
"template": false,
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
}],
"changes": {
"updated_by_id": [null, 1],
"updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
"labels": {
"previous": [{
"id": 206,
"title": "API",
"color": "#ffffff",
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
"updated_at": "2013-12-03T17:15:43Z",
"template": false,
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
}],
"current": [{
"id": 205,
"title": "Platform",
"color": "#123123",
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
"updated_at": "2013-12-03T17:15:43Z",
"template": false,
"description": "Platform related issues",
"type": "ProjectLabel",
"group_id": 41
}]
}
}
}
```
......@@ -1040,7 +1134,7 @@ X-Gitlab-Event: Build Hook
## Testing webhooks
You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
> For example: for triggering `Push Events` your project should have at least one commit.
![Webhook testing](img/webhook_testing.png)
......
......@@ -193,7 +193,7 @@ module Gitlab
def has_local_branches?
gitaly_migrate(:has_local_branches) do |is_enabled|
if is_enabled
gitaly_ref_client.has_local_branches?
gitaly_repository_client.has_local_branches?
else
has_local_branches_rugged?
end
......
......@@ -57,14 +57,6 @@ module Gitlab
branch_names.count
end
# TODO implement a more efficient RPC for this https://gitlab.com/gitlab-org/gitaly/issues/616
def has_local_branches?
request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request).first
response&.names.present?
end
def local_branches(sort_by: nil)
request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo)
request.sort_by = sort_by_param(sort_by) if sort_by
......
......@@ -58,6 +58,13 @@ module Gitlab
request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :create_repository, request)
end
def has_local_branches?
request = Gitaly::HasLocalBranchesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request)
response.value
end
end
end
end
module Gitlab
module HookData
class IssuableBuilder
CHANGES_KEYS = %i[previous current].freeze
attr_accessor :issuable
def initialize(issuable)
@issuable = issuable
end
def build(user: nil, changes: {})
hook_data = {
object_kind: issuable.class.name.underscore,
user: user.hook_attrs,
project: issuable.project.hook_attrs,
object_attributes: issuable.hook_attrs,
labels: issuable.labels.map(&:hook_attrs),
changes: final_changes(changes.slice(*safe_keys)),
# DEPRECATED
repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
}
if issuable.is_a?(Issue)
hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
else
hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
end
hook_data
end
def safe_keys
issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS
end
private
def issuable_builder
case issuable
when Issue
Gitlab::HookData::IssueBuilder
when MergeRequest
Gitlab::HookData::MergeRequestBuilder
end
end
def final_changes(changes_hash)
changes_hash.reduce({}) do |hash, (key, changes_array)|
hash[key] = Hash[CHANGES_KEYS.zip(changes_array)]
hash
end
end
end
end
end
module Gitlab
module HookData
class IssueBuilder
SAFE_HOOK_ATTRIBUTES = %i[
assignee_id
author_id
branch_name
closed_at
confidential
created_at
deleted_at
description
due_date
id
iid
last_edited_at
last_edited_by_id
milestone_id
moved_to_id
project_id
relative_position
state
time_estimate
title
updated_at
updated_by_id
].freeze
SAFE_HOOK_RELATIONS = %i[
assignees
labels
].freeze
attr_accessor :issue
def initialize(issue)
@issue = issue
end
def build
attrs = {
url: Gitlab::UrlBuilder.build(issue),
total_time_spent: issue.total_time_spent,
human_total_time_spent: issue.human_total_time_spent,
human_time_estimate: issue.human_time_estimate,
assignee_ids: issue.assignee_ids,
assignee_id: issue.assignee_ids.first # This key is deprecated
}
issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
.merge!(attrs)
end
end
end
end
module Gitlab
module HookData
class MergeRequestBuilder
SAFE_HOOK_ATTRIBUTES = %i[
assignee_id
author_id
created_at
deleted_at
description
head_pipeline_id
id
iid
last_edited_at
last_edited_by_id
merge_commit_sha
merge_error
merge_params
merge_status
merge_user_id
merge_when_pipeline_succeeds
milestone_id
ref_fetched
source_branch
source_project_id
state
target_branch
target_project_id
time_estimate
title
updated_at
updated_by_id
].freeze
SAFE_HOOK_RELATIONS = %i[
assignee
labels
].freeze
attr_accessor :merge_request
def initialize(merge_request)
@merge_request = merge_request
end
def build
attrs = {
url: Gitlab::UrlBuilder.build(merge_request),
source: merge_request.source_project.try(:hook_attrs),
target: merge_request.target_project.hook_attrs,
last_commit: merge_request.diff_head_commit&.hook_attrs,
work_in_progress: merge_request.work_in_progress?,
total_time_spent: merge_request.total_time_spent,
human_total_time_spent: merge_request.human_total_time_spent,
human_time_estimate: merge_request.human_time_estimate
}
merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
.merge!(attrs)
end
end
end
end
......@@ -2,7 +2,7 @@ module Gitlab
module ImportExport
class ProjectTreeRestorer
# Relations which cannot have both group_id and project_id at the same time
RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze
RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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