Commit 77daed05 authored by Regis's avatar Regis

merge master

parents d46af1d9 632450a4
...@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq-cron', '~> 0.4.4' gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
......
...@@ -126,7 +126,7 @@ GEM ...@@ -126,7 +126,7 @@ GEM
coffee-script-source (1.10.0) coffee-script-source (1.10.0)
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.2)
connection_pool (2.2.0) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
creole (0.5.0) creole (0.5.0)
...@@ -648,10 +648,10 @@ GEM ...@@ -648,10 +648,10 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.2.1) sidekiq (4.2.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.4) sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
...@@ -928,7 +928,7 @@ DEPENDENCIES ...@@ -928,7 +928,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2) sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0) simplecov (= 0.12.0)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
this.CommitFile = (function() { this.CommitFile = (function() {
function CommitFile(file) { function CommitFile(file) {
if ($('.image', file).length) { if ($('.image', file).length) {
new ImageFile(file); new gl.ImageFile(file);
} }
} }
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() { (function() {
this.ImageFile = (function() { gl.ImageFile = (function() {
var prepareFrames; var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2 // Width where images must fits in, for 2-up this gets divided by 2
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
var genericError, genericSuccess, showTooltip; var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) { genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!'); showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border // Clear the selection and blur the trigger so it loses its border
e.clearSelection(); e.clearSelection();
return $(e.trigger).blur(); return $(e.trigger).blur();
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
var originalTitle = $target.data('original-title'); var originalTitle = $target.data('original-title');
$target $target
.attr('title', 'Copied!') .attr('title', 'Copied')
.tooltip('fixTitle') .tooltip('fixTitle')
.tooltip('show') .tooltip('show')
.attr('title', originalTitle) .attr('title', originalTitle)
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
class Diff { class Diff {
constructor() { constructor() {
$('.files .diff-file').singleFileDiff(); const $diffFile = $('.files .diff-file');
$('.files .diff-file').filesCommentButton(); $diffFile.singleFileDiff();
$diffFile.filesCommentButton();
$diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited'); $('.content-wrapper .container-fluid').removeClass('container-limited');
......
...@@ -52,6 +52,10 @@ ...@@ -52,6 +52,10 @@
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}, },
beforeInsert: function(value) { beforeInsert: function(value) {
if (value && !this.setting.skipSpecialCharacterTest) {
var withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
}
if (!GitLab.GfmAutoComplete.dataLoaded) { if (!GitLab.GfmAutoComplete.dataLoaded) {
return this.at; return this.at;
} else { } else {
...@@ -117,6 +121,7 @@ ...@@ -117,6 +121,7 @@
insertTpl: ':${name}:', insertTpl: ':${name}:',
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
skipSpecialCharacterTest: true,
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter, filter: this.DefaultOptions.filter,
...@@ -141,6 +146,7 @@ ...@@ -141,6 +146,7 @@
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
alwaysHighlightFirst: true, alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter, filter: this.DefaultOptions.filter,
...@@ -219,12 +225,13 @@ ...@@ -219,12 +225,13 @@
} }
}; };
})(this), })(this),
insertTpl: '${atwho-at}"${title}"', insertTpl: '${atwho-at}${title}',
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(milestones) { beforeSave: function(milestones) {
return $.map(milestones, function(m) { return $.map(milestones, function(m) {
if (m.title == null) { if (m.title == null) {
...@@ -284,18 +291,11 @@ ...@@ -284,18 +291,11 @@
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(merges) { beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
return "\"" + (sanitize(title)) + "\"";
} else {
return sanitize(title);
}
};
return $.map(merges, function(m) { return $.map(merges, function(m) {
return { return {
title: sanitizeLabelTitle(m.title), title: sanitize(m.title),
color: m.color, color: m.color,
search: "" + m.title search: "" + m.title
}; };
...@@ -308,6 +308,7 @@ ...@@ -308,6 +308,7 @@
at: '/', at: '/',
alias: 'commands', alias: 'commands',
searchKey: 'search', searchKey: 'search',
skipSpecialCharacterTest: true,
displayTpl: function(value) { displayTpl: function(value) {
var tpl = '<li>/${name}'; var tpl = '<li>/${name}';
if (value.aliases.length > 0) { if (value.aliases.length > 0) {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
(function() { (function() {
window.ContributorsStatGraphUtil = { window.ContributorsStatGraphUtil = {
parse_log: function(log) { parse_log: function(log) {
var by_author, by_email, data, entry, i, len, total; var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {}; total = {};
by_author = {}; by_author = {};
by_email = {}; by_email = {};
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
if (total[entry.date] == null) { if (total[entry.date] == null) {
this.add_date(entry.date, total); this.add_date(entry.date, total);
} }
data = by_author[entry.author_name] || by_email[entry.author_email]; normalized_email = entry.author_email.toLowerCase();
data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) { if (data == null) {
data = this.add_author(entry, by_author, by_email); data = this.add_author(entry, by_author, by_email);
} }
...@@ -32,12 +33,14 @@ ...@@ -32,12 +33,14 @@
return collection[date].date = date; return collection[date].date = date;
}, },
add_author: function(author, by_author, by_email) { add_author: function(author, by_author, by_email) {
var data; var data, normalized_email;
data = {}; data = {};
data.author_name = author.author_name; data.author_name = author.author_name;
data.author_email = author.author_email; data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data; by_author[author.author_name] = data;
return by_email[author.author_email] = data; by_email[normalized_email] = data;
return data;
}, },
store_data: function(entry, total, by_author) { store_data: function(entry, total, by_author) {
this.store_commits(total, by_author); this.store_commits(total, by_author);
......
...@@ -40,19 +40,26 @@ ...@@ -40,19 +40,26 @@
$('#modal_merge_info').modal({ $('#modal_merge_info').modal({
show: false show: false
}); });
this.firstCICheck = true;
this.readyForCICheck = false;
this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners(); this.clearEventListeners();
this.addEventListeners(); this.addEventListeners();
this.getCIStatus(false); this.getCIStatus(false);
this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon(); this.retrieveSuccessIcon();
this.pollCIStatus();
this.pollCIEnvironmentsStatus(); this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
});
this.ciEnvironmentStatusInterval = new global.SmartInterval({
callback: this.getCIEnvironmentsStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
notifyPermissions(); notifyPermissions();
} }
...@@ -60,10 +67,6 @@ ...@@ -60,10 +67,6 @@
return $(document).off('page:change.merge_request'); return $(document).off('page:change.merge_request');
}; };
MergeRequestWidget.prototype.cancelPolling = function() {
return this.cancel = true;
};
MergeRequestWidget.prototype.addEventListeners = function() { MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages; var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
...@@ -72,9 +75,6 @@ ...@@ -72,9 +75,6 @@
var page; var page;
page = $('body').data('page').split(':').last(); page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) { if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners(); return _this.clearEventListeners();
} }
}; };
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } else if (data.merge_error) {
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
...@@ -114,6 +114,11 @@ ...@@ -114,6 +114,11 @@
}); });
}; };
MergeRequestWidget.prototype.cancelPolling = function () {
this.ciStatusInterval.cancel();
this.ciEnvironmentStatusInterval.cancel();
};
MergeRequestWidget.prototype.getMergeStatus = function() { MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) { return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data); return $('.mr-state-widget').replaceWith(data);
...@@ -131,18 +136,6 @@ ...@@ -131,18 +136,6 @@
} }
}; };
MergeRequestWidget.prototype.pollCIStatus = function() {
return this.fetchBuildStatusInterval = setInterval(((function(_this) {
return function() {
if (!_this.readyForCICheck) {
return;
}
_this.getCIStatus(true);
return _this.readyForCICheck = false;
};
})(this)), 10000);
};
MergeRequestWidget.prototype.getCIStatus = function(showNotification) { MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this; var _this;
_this = this; _this = this;
...@@ -150,23 +143,17 @@ ...@@ -150,23 +143,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title;
if (_this.cancel) {
return;
}
_this.readyForCICheck = true;
if (data.status === '') { if (data.status === '') {
return; return;
} }
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status; _this.opts.ci_status = data.status;
_this.showCIStatus(data.status); _this.showCIStatus(data.status);
if (data.coverage) { if (data.coverage) {
_this.showCICoverage(data.coverage); _this.showCICoverage(data.coverage);
} }
// The first check should only update the UI, a notification if (showNotification) {
// should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
title = _this.opts.ci_title.preparing; title = _this.opts.ci_title.preparing;
...@@ -184,24 +171,13 @@ ...@@ -184,24 +171,13 @@
return Turbolinks.visit(_this.opts.builds_path); return Turbolinks.visit(_this.opts.builds_path);
}); });
} }
return _this.firstCICheck = false;
} }
}; };
})(this)); })(this));
}; };
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
if (!this.readyForCIEnvironmentCheck) return;
this.getCIEnvironmentsStatus();
this.readyForCIEnvironmentCheck = false;
}, 300000);
};
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => { $.getJSON(this.opts.ci_environments_status_url, (environments) => {
if (this.cancel) return;
this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments); if (environments && environments.length) this.renderEnvironments(environments);
}); });
}; };
...@@ -212,11 +188,11 @@ ...@@ -212,11 +188,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return; if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE); const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (!environment.stop_url) { if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove(); $('.js-stop-env-link', $template).remove();
} }
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else { } else {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
((global) => { ((global) => {
class Pipelines { class Pipelines {
constructor(options) { constructor(options = {}) {
if (options.initTabs && options.tabsOptions) { if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions); new global.LinkedTabs(options.tabsOptions);
...@@ -14,9 +14,11 @@ ...@@ -14,9 +14,11 @@
} }
addMarginToBuildColumns() { addMarginToBuildColumns() {
this.pipelineGraph = document.querySelector('.pipeline-graph'); this.pipelineGraph = document.querySelector('.js-pipeline-graph');
const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
for (buildNodeIndex in secondChildBuildNodes) { const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex]; const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling; const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build'); const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
} }
this.pipelineGraph.classList.remove('hidden'); this.pipelineGraph.classList.remove('hidden');
} }
} }
......
...@@ -7,24 +7,31 @@ ...@@ -7,24 +7,31 @@
(() => { (() => {
class SmartInterval { class SmartInterval {
/** /**
* @param { function } callback Function to be called on each iteration (required) * @param { function } opts.callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily * when the page is hidden
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } opts.lazyStart Configure if timer is initialized on
* instantiation or lazily
* @param { boolean } opts.immediateExecution Configure if callback should
* be executed before the first interval.
*/ */
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { constructor(opts = {}) {
this.cfg = { this.cfg = {
callback, callback: opts.callback,
startingInterval, startingInterval: opts.startingInterval,
maxInterval, maxInterval: opts.maxInterval,
incrementByFactorOf, hiddenInterval: opts.hiddenInterval,
lazyStart, incrementByFactorOf: opts.incrementByFactorOf,
lazyStart: opts.lazyStart,
immediateExecution: opts.immediateExecution,
}; };
this.state = { this.state = {
intervalId: null, intervalId: null,
currentInterval: startingInterval, currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible', pageVisibility: 'visible',
}; };
...@@ -36,6 +43,11 @@ ...@@ -36,6 +43,11 @@
const cfg = this.cfg; const cfg = this.cfg;
const state = this.state; const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => { state.intervalId = window.setInterval(() => {
cfg.callback(); cfg.callback();
...@@ -54,14 +66,29 @@ ...@@ -54,14 +66,29 @@
this.stopTimer(); this.stopTimer();
} }
onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
}
}
// start a timer, using the existing interval // start a timer, using the existing interval
resume() { resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start(); this.start();
} }
onVisibilityVisible() {
this.cancel();
this.start();
}
destroy() { destroy() {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload'); $(document).off('visibilitychange').off('page:before-unload');
} }
...@@ -80,11 +107,7 @@ ...@@ -80,11 +107,7 @@
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab no longer shown (prevents cached pages from polling)
$(document) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
} }
initPageUnloadHandling() { initPageUnloadHandling() {
...@@ -92,10 +115,11 @@ ...@@ -92,10 +115,11 @@
$(document).on('page:before-unload', () => this.cancel()); $(document).on('page:before-unload', () => this.cancel());
} }
handleVisibilityChange() { handleVisibilityChange(e) {
const state = this.state; this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() ?
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this); intervalAction.apply(this);
} }
...@@ -111,6 +135,7 @@ ...@@ -111,6 +135,7 @@
incrementInterval() { incrementInterval() {
const cfg = this.cfg; const cfg = this.cfg;
const currentInterval = this.getCurrentInterval(); const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf; let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) { if (nextInterval > cfg.maxInterval) {
...@@ -120,6 +145,8 @@ ...@@ -120,6 +145,8 @@
this.setCurrentInterval(nextInterval); this.setCurrentInterval(nextInterval);
} }
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() { stopTimer() {
const state = this.state; const state = this.state;
......
...@@ -486,6 +486,7 @@ $jq-ui-default-color: #777; ...@@ -486,6 +486,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 14px;
/* /*
* Lint * Lint
......
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
.color-label { .color-label {
padding: 6px 10px; padding: 6px 10px;
border-radius: $label-border-radius;
} }
} }
......
...@@ -104,7 +104,8 @@ ...@@ -104,7 +104,8 @@
} }
.color-label { .color-label {
padding: 3px 4px; padding: 3px 7px;
border-radius: $label-border-radius;
} }
.dropdown-labels-error { .dropdown-labels-error {
......
.notification-list-item { .notification-list-item {
line-height: 34px; line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-align-right;
}
} }
.notification { .notification {
......
...@@ -188,6 +188,10 @@ ...@@ -188,6 +188,10 @@
margin-left: 10px; margin-left: 10px;
} }
.notification-dropdown .dropdown-menu {
@extend .dropdown-menu-align-right;
}
.download-button { .download-button {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
margin-left: 0; margin-left: 0;
......
.snippet-row {
.title {
margin-bottom: 2px;
}
.snippet-filename {
padding: 0 2px;
}
}
.snippet-form-holder .file-holder .file-title { .snippet-form-holder .file-holder .file-title {
padding: 2px; padding: 2px;
} }
...@@ -24,11 +34,17 @@ ...@@ -24,11 +34,17 @@
padding-bottom: $gl-padding; padding-bottom: $gl-padding;
} }
.snippet-header {
padding: $gl-padding 0;
}
.snippet-title { .snippet-title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
padding: $gl-padding; }
padding-left: 0;
.snippet-edited-ago {
color: $gray-darkest;
} }
.snippet-actions { .snippet-actions {
......
...@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end end
def update def update
release.update_attributes(release_params) # Release belongs to Tag which is not active record object,
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
release.update_attributes(release_params)
else
release.destroy
end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end end
......
...@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@snippets = SnippetsFinder.new.execute(current_user, { @snippets = SnippetsFinder.new.execute(
current_user,
filter: :by_project, filter: :by_project,
project: @project project: @project,
}) scope: params[:scope]
)
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
end end
......
...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController ...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end end
end end
def destroy
super
# hide the signed_out notice
flash[:notice] = nil
end
private private
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
......
class SnippetsFinder class SnippetsFinder
def execute(current_user, params = {}) def execute(current_user, params = {})
filter = params[:filter] filter = params[:filter]
user = params.fetch(:user, current_user)
case filter case filter
when :all then when :all then
snippets(current_user).fresh snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, user, params[:scope])
when :by_project when :by_project
by_project(current_user, params[:project]) by_project(current_user, params[:project], params[:scope])
end end
end end
...@@ -29,35 +32,35 @@ class SnippetsFinder ...@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope) def by_user(current_user, user, scope)
snippets = user.snippets.fresh snippets = user.snippets.fresh
return snippets.are_public unless current_user if current_user
include_private = user == current_user
if user == current_user by_scope(snippets, scope, include_private)
case scope
when 'are_internal' then
snippets.are_internal
when 'are_private' then
snippets.are_private
when 'are_public' then
snippets.are_public
else
snippets
end
else else
snippets.public_and_internal snippets.are_public
end end
end end
def by_project(current_user, project) def by_project(current_user, project, scope)
snippets = project.snippets.fresh snippets = project.snippets.fresh
if current_user if current_user
if project.team.member?(current_user) || current_user.admin? include_private = project.team.member?(current_user) || current_user.admin?
snippets by_scope(snippets, scope, include_private)
else
snippets.public_and_internal
end
else else
snippets.are_public snippets.are_public
end end
end end
def by_scope(snippets, scope = nil, include_private = false)
case scope.to_s
when 'are_private'
include_private ? snippets.are_private : Snippet.none
when 'are_internal'
snippets.are_internal
when 'are_public'
snippets.are_public
else
include_private ? snippets : snippets.public_and_internal
end
end
end end
...@@ -16,7 +16,7 @@ module ButtonHelper ...@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to Clipboard' title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
......
...@@ -159,6 +159,11 @@ module GitlabRoutingHelper ...@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups # Groups
## Members ## Members
......
...@@ -8,6 +8,17 @@ module SnippetsHelper ...@@ -8,6 +8,17 @@ module SnippetsHelper
end end
end end
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
def subject_snippets_path(subject = nil, opts = nil)
if subject.is_a?(Project)
namespace_project_snippets_path(subject.namespace, subject, opts)
else # assume subject === User
dashboard_snippets_path(opts)
end
end
# Get an array of line numbers surrounding a matching # Get an array of line numbers surrounding a matching
# line, bounded by min/max. # line, bounded by min/max.
# #
......
...@@ -7,6 +7,7 @@ module Routable ...@@ -7,6 +7,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route validates_associated :route
validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed? before_validation :update_route_path, if: :full_path_changed?
end end
...@@ -28,17 +29,17 @@ module Routable ...@@ -28,17 +29,17 @@ module Routable
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take where_full_path_in([path]).reorder(order_sql).take
end end
# Builds a relation to find multiple objects by their full paths. # Builds a relation to find multiple objects by their full paths.
# #
# Usage: # Usage:
# #
# Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def where_paths_in(paths) def where_full_path_in(paths)
wheres = [] wheres = []
cast_lower = Gitlab::Database.postgresql? cast_lower = Gitlab::Database.postgresql?
......
...@@ -3,7 +3,8 @@ require "addressable/uri" ...@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com" ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token, :enable_ssl_verification prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated? validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
......
class DroneCiService < CiService class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated? validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
......
class EmailsOnPushService < Service class EmailsOnPushService < Service
prop_accessor :send_from_committer_email boolean_accessor :send_from_committer_email
prop_accessor :disable_diffs boolean_accessor :disable_diffs
prop_accessor :recipients prop_accessor :recipients
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :activated?
...@@ -24,20 +24,20 @@ class EmailsOnPushService < Service ...@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind]) return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async( EmailsOnPushWorker.perform_async(
project_id, project_id,
recipients, recipients,
push_data, push_data,
send_from_committer_email: send_from_committer_email?, send_from_committer_email: send_from_committer_email?,
disable_diffs: disable_diffs? disable_diffs: disable_diffs?
) )
end end
def send_from_committer_email? def send_from_committer_email?
self.send_from_committer_email == "1" Gitlab::Utils.to_boolean(self.send_from_committer_email)
end end
def disable_diffs? def disable_diffs?
self.disable_diffs == "1" Gitlab::Utils.to_boolean(self.disable_diffs)
end end
def fields def fields
......
...@@ -8,8 +8,8 @@ class HipchatService < Service ...@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd ul ol li dl dt dd
] ]
prop_accessor :token, :room, :server, :notify, :color, :api_version prop_accessor :token, :room, :server, :color, :api_version
boolean_accessor :notify_only_broken_builds boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
def initialize_properties def initialize_properties
...@@ -75,7 +75,7 @@ class HipchatService < Service ...@@ -75,7 +75,7 @@ class HipchatService < Service
end end
def message_options(data = nil) def message_options(data = nil)
{ notify: notify.present? && notify == '1', color: message_color(data) } { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end end
def create_message(data) def create_message(data)
......
...@@ -2,7 +2,8 @@ require 'uri' ...@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :colorize_messages, :recipients, :channels prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :activated?
before_validation :get_channels before_validation :get_channels
......
...@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService ...@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title] entity_title = data[:entity][:title]
project_name = data[:project][:name] project_name = data[:project][:name]
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title) link_props = build_remote_link_props(url: entity_url, title: link_title)
......
...@@ -304,10 +304,6 @@ class User < ActiveRecord::Base ...@@ -304,10 +304,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token personal_access_token.user if personal_access_token
end end
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
# Returns a user for the given SSH key. # Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id) def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
......
...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
end end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
end end
......
...@@ -35,7 +35,7 @@ module Commits ...@@ -35,7 +35,7 @@ module Commits
success success
else else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg raise ChangeError, error_msg
end end
end end
......
%ul.nav-links .top-area
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do %ul.nav-links
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
Your Snippets = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
= nav_link(page: explore_snippets_path) do Your Snippets
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do = nav_link(page: explore_snippets_path) do
Explore Snippets = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets
- if current_user
.nav-controls.hidden-xs
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
New snippet
...@@ -2,41 +2,11 @@ ...@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path - header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head' = render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
.nav-block .visible-xs
.controls.hidden-xs &nbsp;
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
= icon('plus') New snippet
New snippet
.nav-links.snippet-scope-menu = render partial: 'snippets/snippets', locals: { link_project: true }
%li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All
%span.badge
= current_user.snippets.count
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private
%span.badge
= current_user.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
%span.badge
= current_user.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
.visible-xs
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
= icon('plus')
New snippet
= render 'snippets/snippets'
...@@ -6,12 +6,4 @@ ...@@ -6,12 +6,4 @@
- else - else
= render 'explore/head' = render 'explore/head'
.row-content-block = render partial: 'snippets/snippets', locals: { link_project: true }
- if current_user
= link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
New snippet
.oneline
Public snippets created by you and other users are listed here
= render 'snippets/snippets'
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
- if group_issues(@group).exists? - if group_issues(@group).exists?
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls - if current_user
- if current_user .nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss') = icon('rss')
%span.icon-label %span.icon-label
Subscribe Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls - if current_user
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
......
- page_title @path.split("/").reverse.map(&:humanize) - page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki .documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") = markdown @markdown
- if current_user - if current_user
- can_admin_group = can?(current_user, :admin_group, @group) - can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member)
- if can_admin_group || can_edit || can_leave - if can_admin_group || can_edit
.controls .controls
.dropdown.group-settings-dropdown .dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
...@@ -14,13 +12,7 @@ ...@@ -14,13 +12,7 @@
- if can_admin_group - if can_admin_group
= nav_link(path: 'groups#projects') do = nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects' = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- if (can_edit || can_leave) && can_admin_group - if can_edit && can_admin_group
%li.divider %li.divider
- if can_edit
%li %li
= link_to 'Edit Group', edit_group_path(@group) = link_to 'Edit Group', edit_group_path(@group)
- if can_leave
%li
= link_to polymorphic_path([:leave, @group, :members]),
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
...@@ -6,23 +6,14 @@ ...@@ -6,23 +6,14 @@
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
-# We don't use @project.team.find_member because it searches for group members too...
- member = @project.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit = render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit || can_leave - if can_edit
%li.divider %li.divider
- if can_edit %li
%li = link_to edit_project_path(@project) do
= link_to edit_project_path(@project) do Edit Project
Edit Project
- if can_leave
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
.fade-left .fade-left
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%br %br
.clearfix .clearfix
.form-group.pull-left.global-notification-setting .form-group.pull-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix .clearfix
......
- is_playable = subject.playable? && can?(current_user, :update_build, @project) - is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable - if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
= ci_icon_for_status('play') = ci_icon_for_status('play')
.ci-status-text= subject.name .ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project) - elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"} %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status) = ci_icon_for_status(subject.status)
.ci-status-text= subject.name .ci-status-text= subject.name
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)} %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body .modal-body
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch .form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label' = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -23,12 +23,11 @@ ...@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
.js-create-merge-request-container .js-create-merge-request-container
.checkbox .checkbox
- nonce = SecureRandom.hex = label_tag do
= label_tag "create_merge_request-#{nonce}" do = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes Start a <strong>new merge request</strong> with these changes
- else - else
= hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions .form-actions
= submit_tag label, class: 'btn btn-create' = submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
......
.page-content-header .page-content-header
.header-main-content .header-main-content
%strong Commit %strong
%strong.monospace.js-details-short= @commit.short_id
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
%span.text-expander
\...
%span.js-details-content.hide
%strong.monospace.commit-hash-full= @commit.id
= clipboard_button(clipboard_text: @commit.id) = clipboard_button(clipboard_text: @commit.id)
= @commit.short_id
%span.hidden-xs authored %span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)} #{time_ago_with_tooltip(@commit.authored_date)}
%span by %span by
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
in in
= time_interval_in_words pipeline.duration = time_interval_in_words pipeline.duration
.row-content-block.build-content.middle-block.hidden .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline = render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
......
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
such as compressing file revisions and removing unreachable objects. such as compressing file revisions and removing unreachable objects.
.col-lg-9 .col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save" method: :post, class: "btn btn-default"
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
......
%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } %a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
- if subject.target_url - if subject.target_url
= link_to subject.target_url do = link_to subject.target_url do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"} %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
.note-text.md .note-text.md
= preserve do = preserve do
= note.redacted_note_html = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
.note-awards .note-awards
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.tab-content .tab-content
#js-tab-pipeline.tab-pane #js-tab-pipeline.tab-pane
.build-content.middle-block .build-content.middle-block.js-pipeline-graph
= render "projects/pipelines/graph", pipeline: pipeline = render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane #js-tab-builds.tab-pane
......
.hidden-xs .hidden-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
New snippet
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit Edit
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......
- page_title "Snippets" - page_title "Snippets"
.sub-header-block - if current_user
- if can?(current_user, :create_project_snippet, @project) .top-area
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - include_private = @project.team.member?(current_user) || current_user.admin?
New snippet = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
.nav-controls.hidden-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
New snippet
.oneline - if can?(current_user, :create_project_snippet, @project)
Share code pastes with others out of git repository .visible-xs
&nbsp;
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
New snippet
= render 'snippets/snippets' = render 'snippets/snippets'
...@@ -5,14 +5,11 @@ ...@@ -5,14 +5,11 @@
%tr %tr
%th Name %th Name
%th.hidden-xs %th.hidden-xs
.pull-left Last Commit .pull-left Last commit
.last-commit.hidden-sm.pull-left .last-commit.hidden-sm.pull-left
&nbsp; %small.light
%i.fa.fa-angle-right = clipboard_button(clipboard_text: @commit.id)
&nbsp;
%small.light
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= time_ago_with_tooltip(@commit.committed_date) = time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title = @commit.full_title
%small.commit-history-link-spacer &#124; %small.commit-history-link-spacer &#124;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info .snippet-info
= "##{snippet_title.id}" = snippet_title.to_reference
%span %span
by by
= link_to user_snippets_path(snippet_title.author) do = link_to user_snippets_path(snippet_title.author) do
......
- if can?(current_user, :request_access, source) - model_name = source.model_name.to_s.downcase
- if requester = source.requesters.find_by(user_id: current_user.id)
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
method: :delete, = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
data: { confirm: remove_member_message(requester) }, method: :delete,
class: 'btn' data: { confirm: leave_confirmation_message(source) },
- else class: 'btn'
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]), - elsif requester = source.requesters.find_by(user_id: current_user.id)
method: :post, = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
class: 'btn' method: :delete,
data: { confirm: remove_member_message(requester) },
class: 'btn'
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn'
- left_align = local_assigns[:left_align]
- if notification_setting - if notification_setting
.dropdown.notification-dropdown .dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
...@@ -19,7 +18,7 @@ ...@@ -19,7 +18,7 @@
= notification_title(notification_setting.level) = notification_title(notification_setting.level)
= icon("caret-down") = icon("caret-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do = content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting = render "shared/notifications/custom_notifications", notification_setting: notification_setting
- left_align = local_assigns[:left_align] %ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
- NotificationSetting.levels.each_key do |level| - NotificationSetting.levels.each_key do |level|
- next if level == "custom" - next if level == "custom"
- next if level == "global" && notification_setting.source.nil? - next if level == "global" && notification_setting.source.nil?
......
...@@ -8,10 +8,6 @@ ...@@ -8,10 +8,6 @@
%span.creator %span.creator
authored authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- if @snippet.updated_at != @snippet.created_at
%span
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions .snippet-actions
...@@ -20,5 +16,9 @@ ...@@ -20,5 +16,9 @@
- else - else
= render "snippets/actions" = render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0 .snippet-header
= markdown_field(@snippet, :title) %h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row %li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title .title
= link_to reliable_snippet_path(snippet) do = link_to reliable_snippet_path(snippet) do
= snippet.title = snippet.title
- if snippet.private? - if snippet.file_name
%span.label.label-gray.hidden-xs %span.snippet-filename.monospace.hidden-xs
= icon('lock') = snippet.file_name
private
%span.monospace.pull-right.hidden-xs
= snippet.file_name
%ul.controls.visible-xs %ul.controls
%li %li
- note_count = snippet.notes.user.count - note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
...@@ -22,11 +21,17 @@ ...@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level) = visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false) = visibility_level_icon(snippet.visibility_level, fw: false)
%small.pull-right.cgray.hidden-xs .snippet-info
- if snippet.project_id? #{snippet.to_reference} &middot;
= link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
by
.snippet-info.hidden-xs
= link_to user_snippets_path(snippet.author) do = link_to user_snippets_path(snippet.author) do
= snippet.author_name = snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)} - if link_project && snippet.project_id?
%span.hidden-xs
in
= link_to namespace_project_path(snippet.project.namespace, snippet.project) do
= snippet.project.name_with_namespace
.pull-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
.hidden-xs .hidden-xs
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
New snippet
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_personal_snippet, @snippet) - if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit Edit
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
New snippet
- if current_user - if current_user
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......
- remote = local_assigns.fetch(:remote, false) - remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder .snippets-list-holder
%ul.content-list %ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty? - if @snippets.empty?
%li %li
.nothing-here-block Nothing here. .nothing-here-block Nothing here.
......
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to subject_snippets_path(subject) do
All
%span.badge
- if include_private
= subject.snippets.count
- else
= subject.snippets.public_and_internal.count
- if include_private
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
%span.badge
= subject.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
%span.badge
= subject.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
%span.badge
= subject.snippets.are_public.count
---
title: group authors in contribution graph with case insensitive email handle comparison
merge_request: 8021
author:
---
title: Moved Leave Project and Leave Group buttons to access_request_buttons from
the settings dropdown
merge_request: 7600
author:
---
title: Use SmartInterval for MR widget and improve visibilitychange functionality
merge_request: 7762
author:
---
title: Prevent user creating issue or MR without signing in for a group
merge_request: 7902
author:
---
title: 'fix: removed signed_out notification'
merge_request: 7958
author: jnoortheen
---
title: Changed Housekeeping button on project settings page to default styling
merge_request:
author: Ryan Harris
---
title: 'API: Memoize the current_user so that sudo can work properly'
merge_request: 8017
author:
---
title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
merge_request: 8009
author:
---
title: Allow all alphanumeric characters in file names
merge_request: 8002
author: winniehell
---
title: 'API: Ability to cherry pick a commit'
merge_request: 8047
author: Robert Schilling
---
title: 'API: Simple representation of group''s projects'
merge_request: 8060
author: Robert Schilling
---
title: Replace static fixture for awards_handler_spec
merge_request: 7661
author: winniehell
---
title: For single line git commit messages, the close quote should be on the same
line as the open quote
merge_request:
author:
---
title: 'API: Endpoint to expose personal snippets as /snippets'
merge_request: 6373
author: Bernard Guyzmo Pratz
---
title: Allow to delete tag release note
merge_request:
author:
---
title: "fix display hook error message"
merge_request: 7775
author: basyura
---
title: Allow branch names with dots on API endpoint
merge_request:
author:
---
title: Avoid escaping relative links in Markdown twice
merge_request: 7940
author: winniehell
...@@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({} ...@@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *' Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
......
require 'sidekiq/web' require 'sidekiq/web'
require 'sidekiq/cron/web' require 'sidekiq/cron/web'
require 'api/api'
require 'constraints/group_url_constrainer' require 'constraints/group_url_constrainer'
Rails.application.routes.draw do Rails.application.routes.draw do
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));'
end
def down
return unless Gitlab::Database.postgresql?
remove_index :routes, name: :index_on_routes_lower_path
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161202152035) do ActiveRecord::Schema.define(version: 20161212142807) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
# Documentation # GitLab Community Edition documentation
## User documentation ## User documentation
......
...@@ -183,6 +183,44 @@ Example response: ...@@ -183,6 +183,44 @@ Example response:
} }
``` ```
## Cherry pick a commit
> [Introduced][ce-8047] in GitLab 8.15.
Cherry picks a commit to a given branch.
```
POST /projects/:id/repository/commits/:sha/cherry_pick
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash |
| `branch` | string | yes | The name of the branch |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
```
Example response:
```json
{
"id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
"short_id": "8b090c1b",
"title": "Feature added",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2016-12-12T20:10:39.000+01:00",
"committer_name": "Administrator",
"committer_email": "admin@example.com",
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
}
```
## Get the diff of a commit ## Get the diff of a commit
Get the diff of a commit in a project. Get the diff of a commit in a project.
...@@ -438,3 +476,4 @@ Example response: ...@@ -438,3 +476,4 @@ Example response:
``` ```
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit" [ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
...@@ -50,12 +50,17 @@ GET /groups/:id/projects ...@@ -50,12 +50,17 @@ GET /groups/:id/projects
Parameters: Parameters:
- `archived` (optional) - if passed, limit by archived status | Attribute | Type | Required | Description |
- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` | --------- | ---- | -------- | ----------- |
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` | `id` | integer/string | yes | The ID or path of a group |
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` | `archived` | boolean | no | Limit by archived status |
- `search` (optional) - Return list of authorized projects according to a search criteria | `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
Example response:
```json ```json
[ [
......
...@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id ...@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request | | `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
``` ```
## Accept MR ## Accept MR
......
...@@ -139,6 +139,43 @@ Get Buildkite service settings for a project. ...@@ -139,6 +139,43 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite GET /projects/:id/services/buildkite
``` ```
## Build-Emails
Get emails for GitLab CI builds.
### Create/Edit Build-Emails service
Set Build-Emails service for a project.
```
PUT /projects/:id/services/builds-email
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
### Delete Build-Emails service
Delete Build-Emails service for a project.
```
DELETE /projects/:id/services/builds-email
```
### Get Build-Emails service settings
Get Build-Emails service settings for a project.
```
GET /projects/:id/services/builds-email
```
## Campfire ## Campfire
Simple web-based real-time group chat Simple web-based real-time group chat
...@@ -476,12 +513,11 @@ PUT /projects/:id/services/jira ...@@ -476,12 +513,11 @@ PUT /projects/:id/services/jira
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `active` | boolean| no | Enable/disable the JIRA service. |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | | `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service ### Delete JIRA service
...@@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project. ...@@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira DELETE /projects/:id/services/jira
``` ```
## Mattermost Slash Commands
Ability to receive slash commands from a Mattermost chat instance.
### Create/Edit Mattermost Slash Command service
Set Mattermost Slash Command for a project.
```
PUT /projects/:id/services/mattermost-slash-commands
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
### Delete Mattermost Slash Command service
Delete Mattermost Slash Command service for a project.
```
DELETE /projects/:id/services/mattermost-slash-commands
```
### Get Mattermost Slash Command service settings
Get Mattermost Slash Command service settings for a project.
```
GET /projects/:id/services/mattermost-slash-commands
```
## Pipeline-Emails
Get emails for GitLab CI pipelines.
### Create/Edit Pipeline-Emails service
Set Pipeline-Emails service for a project.
```
PUT /projects/:id/services/pipelines-email
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
Delete Pipeline-Emails service for a project.
```
DELETE /projects/:id/services/pipelines-email
```
### Get Pipeline-Emails service settings
Get Pipeline-Emails service settings for a project.
```
GET /projects/:id/services/pipelines-email
```
## PivotalTracker ## PivotalTracker
Project Management Software (Source Commits Endpoint) Project Management Software (Source Commits Endpoint)
......
# Snippets
> [Introduced][ce-6373] in GitLab 8.15.
### Snippet visibility level
Snippets in GitLab can be either private, internal, or public.
You can set it with the `visibility_level` field in the snippet.
Constants for snippet visibility levels are:
| Visibility | Visibility level | Description |
| ---------- | ---------------- | ----------- |
| Private | `0` | The snippet is visible only to the snippet creator |
| Internal | `10` | The snippet is visible for any logged in user |
| Public | `20` | The snippet can be accessed without any authentication |
## List snippets
Get a list of current user's snippets.
```
GET /snippets
```
## Single snippet
Get a single snippet.
```
GET /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Create new snippet
Creates a new snippet. The user must have permission to create new snippets.
```
POST /snippets
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | String | yes | The title of a snippet |
| `file_name` | String | yes | The name of a snippet file |
| `content` | String | yes | The content of a snippet |
| `visibility_level` | Integer | yes | The snippet's visibility |
``` bash
curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
```
Example response:
``` json
{
"id": 1,
"title": "This is a snippet",
"file_name": "test.txt",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Update snippet
Updates an existing snippet. The user must have permission to change an existing snippet.
```
PUT /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
| `title` | String | no | The title of a snippet |
| `file_name` | String | no | The name of a snippet file |
| `content` | String | no | The content of a snippet |
| `visibility_level` | Integer | no | The snippet's visibility |
``` bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Delete snippet
Deletes an existing snippet.
```
DELETE /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
```
upon successful delete a `204 No content` HTTP code shall be expected, with no data,
but if the snippet is non-existent, a `404 Not Found` will be returned.
## Explore all public snippets
```
GET /snippets/public
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `per_page` | Integer | no | number of snippets to return per page |
| `page` | Integer | no | the page to retrieve |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
```
Example response:
``` json
[
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
"id": 12,
"name": "Libby Rolfson",
"state": "active",
"username": "elton_wehner",
"web_url": "http://localhost:3000/elton_wehner"
},
"created_at": "2016-11-25T16:53:34.504Z",
"file_name": "oconnerrice.rb",
"id": 49,
"raw_url": "http://localhost:3000/snippets/49/raw",
"title": "Ratione cupiditate et laborum temporibus.",
"updated_at": "2016-11-25T16:53:34.504Z",
"web_url": "http://localhost:3000/snippets/49"
},
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
"id": 16,
"name": "Llewellyn Flatley",
"state": "active",
"username": "adaline",
"web_url": "http://localhost:3000/adaline"
},
"created_at": "2016-11-25T16:53:34.479Z",
"file_name": "muellershields.rb",
"id": 48,
"raw_url": "http://localhost:3000/snippets/48/raw",
"title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
"updated_at": "2016-11-25T16:53:34.479Z",
"web_url": "http://localhost:3000/snippets/48"
}
]
```
...@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an ...@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
also dynamically URL. also dynamically URL.
The details of the Review Apps implementation depend widely on your real The details of the Review Apps implementation depend widely on your real
technology stack and on your deployment process. The simplest case it to technology stack and on your deployment process. The simplest case is to
deploy a simple static HTML website, but it will not be that straightforward deploy a simple static HTML website, but it will not be that straightforward
when your app is using a database for example. To make a branch be deployed when your app is using a database for example. To make a branch be deployed
on a temporary instance and booting up this instance with all required software on a temporary instance and booting up this instance with all required software
......
@admin
Feature: Admin Settings
Background:
Given I sign in as an admin
And I visit admin settings page
Scenario: Change application settings
When I modify settings and save form
Then I should see application settings saved
Scenario: Change Slack Service Template settings
When I click on "Service Templates"
And I click on "Slack" service
And I fill out Slack settings
Then I check all events and submit form
And I should see service template settings saved
Then I click on "Slack" service
And I should see all checkboxes checked
And I should see Slack settings saved
...@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps ...@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end end
step 'I click link "New snippet"' do step 'I click link "New snippet"' do
click_link "New snippet" first(:link, "New snippet").click
end end
step 'I click link "Snippet one"' do step 'I click link "Snippet one"' do
......
...@@ -203,10 +203,6 @@ module SharedPaths ...@@ -203,10 +203,6 @@ module SharedPaths
visit admin_teams_path visit admin_teams_path
end end
step 'I visit admin settings page' do
visit admin_application_settings_path
end
step 'I visit spam logs page' do step 'I visit spam logs page' do
visit admin_spam_logs_path visit admin_spam_logs_path
end end
......
...@@ -64,6 +64,7 @@ module API ...@@ -64,6 +64,7 @@ module API
mount ::API::Session mount ::API::Session
mount ::API::Settings mount ::API::Settings
mount ::API::SidekiqMetrics mount ::API::SidekiqMetrics
mount ::API::Snippets
mount ::API::Subscriptions mount ::API::Subscriptions
mount ::API::SystemHooks mount ::API::SystemHooks
mount ::API::Tags mount ::API::Tags
......
...@@ -23,9 +23,9 @@ module API ...@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
get ':id/repository/branches/:branch' do get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch not_found!("Branch") unless branch
...@@ -39,11 +39,11 @@ module API ...@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end end
put ':id/repository/branches/:branch/protect' do put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project authorize_admin_project
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
...@@ -76,9 +76,9 @@ module API ...@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch success Entities::RepoBranch
end end
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
put ':id/repository/branches/:branch/unprotect' do put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project authorize_admin_project
branch = user_project.repository.find_branch(params[:branch]) branch = user_project.repository.find_branch(params[:branch])
...@@ -112,9 +112,9 @@ module API ...@@ -112,9 +112,9 @@ module API
desc 'Delete a branch' desc 'Delete a branch'
params do params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' requires :branch, type: String, desc: 'The name of the branch'
end end
delete ":id/repository/branches/:branch" do delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project authorize_push_project
result = DeleteBranchService.new(user_project, current_user). result = DeleteBranchService.new(user_project, current_user).
......
require 'mime/types' require 'mime/types'
module API module API
# Projects commits API
class Commits < Grape::API class Commits < Grape::API
include PaginationParams include PaginationParams
...@@ -121,6 +120,41 @@ module API ...@@ -121,6 +120,41 @@ module API
present paginate(notes), with: Entities::CommitNote present paginate(notes), with: Entities::CommitNote
end end
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
success Entities::RepoCommit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
post ':id/repository/commits/:sha/cherry_pick' do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
commit_params = {
commit: commit,
create_merge_request: false,
source_project: user_project,
source_branch: commit.cherry_pick_branch_name,
target_branch: params[:branch]
}
result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
else
render_api_error!(result[:message], 400)
end
end
desc 'Post comment to commit' do desc 'Post comment to commit' do
success Entities::CommitNote success Entities::CommitNote
end end
......
...@@ -201,6 +201,19 @@ module API ...@@ -201,6 +201,19 @@ module API
end end
end end
class PersonalSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
expose :web_url do |snippet|
Gitlab::UrlBuilder.build(snippet)
end
expose :raw_url do |snippet|
Gitlab::UrlBuilder.build(snippet) + "/raw"
end
end
class ProjectEntity < Grape::Entity class ProjectEntity < Grape::Entity
expose :id, :iid expose :id, :iid
expose(:project_id) { |entity| entity.project.id } expose(:project_id) { |entity| entity.project.id }
......
module API module API
class Groups < Grape::API class Groups < Grape::API
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate! }
helpers do helpers do
...@@ -117,12 +117,24 @@ module API ...@@ -117,12 +117,24 @@ module API
success Entities::Project success Entities::Project
end end
params do params do
optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
default: 'created_at', desc: 'Return projects ordered by field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order'
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
use :pagination use :pagination
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group).execute(current_user)
present paginate(projects), with: Entities::Project, user: current_user projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, user: current_user
end end
desc 'Transfer a project to the group namespace. Available only for admin.' do desc 'Transfer a project to the group namespace. Available only for admin.' do
......
...@@ -8,67 +8,23 @@ module API ...@@ -8,67 +8,23 @@ module API
SUDO_HEADER = "HTTP_SUDO" SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
def warden
env['warden']
end
# Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def declared_params(options = {}) def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options) options = { include_parent_namespaces: false }.merge(options)
declared(params, options).to_h.symbolize_keys declared(params, options).to_h.symbolize_keys
end end
def find_user_by_private_token
token = private_token
return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def current_user def current_user
@current_user ||= find_user_by_private_token return @current_user if defined?(@current_user)
@current_user ||= doorkeeper_guard
@current_user ||= find_user_from_warden
unless @current_user && Gitlab::UserAccess.new(@current_user).allowed? @current_user = initial_current_user
return nil
end
identifier = sudo_identifier
if identifier
# We check for private_token because we cannot allow PAT to be used
forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
@impersonator = @current_user sudo!
@current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
end
@current_user @current_user
end end
def sudo_identifier def sudo?
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] initial_current_user != current_user
# Regex for integers
if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
end
end end
def user_project def user_project
...@@ -79,6 +35,14 @@ module API ...@@ -79,6 +35,14 @@ module API
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end end
def find_user(id)
if id =~ /^\d+$/
User.find_by(id: id)
else
User.find_by(username: id)
end
end
def find_project(id) def find_project(id)
if id =~ /^\d+$/ if id =~ /^\d+$/
Project.find_by(id: id) Project.find_by(id: id)
...@@ -97,17 +61,6 @@ module API ...@@ -97,17 +61,6 @@ module API
end end
end end
def project_service(project = user_project)
@project_service ||= project.find_or_initialize_service(params[:service_slug].underscore)
@project_service || not_found!("Service")
end
def service_attributes
@service_attributes ||= project_service.fields.inject([]) do |arr, hash|
arr << hash[:name].to_sym
end
end
def find_group(id) def find_group(id)
if id =~ /^\d+$/ if id =~ /^\d+$/
Group.find_by(id: id) Group.find_by(id: id)
...@@ -349,8 +302,99 @@ module API ...@@ -349,8 +302,99 @@ module API
private private
def private_token_used? def private_token
private_token == @current_user.private_token params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
def warden
env['warden']
end
# Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
token = private_token
return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
@initial_current_user ||= find_user_by_private_token
@initial_current_user ||= doorkeeper_guard
@initial_current_user ||= find_user_from_warden
unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
@initial_current_user = nil
end
@initial_current_user
end
def sudo!
return unless sudo_identifier
return unless initial_current_user
unless initial_current_user.is_admin?
forbidden!('Must be admin to use sudo')
end
# Only private tokens should be used for the SUDO feature
unless private_token == initial_current_user.private_token
forbidden!('Private token must be specified in order to use sudo')
end
sudoed_user = find_user(sudo_identifier)
if sudoed_user
@current_user = sudoed_user
else
not_found!("No user id or username for: #{sudo_identifier}")
end
end
def sudo_identifier
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data)
end
def pagination_links(paginated_data)
request_url = request.url.split('?').first
request_params = params.clone
request_params[:per_page] = paginated_data.limit_value
links = []
request_params[:page] = paginated_data.current_page - 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
request_params[:page] = paginated_data.current_page + 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
request_params[:page] = 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
request_params[:page] = paginated_data.total_pages
links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
links.join(', ')
end end
def secret_token def secret_token
......
...@@ -143,8 +143,8 @@ module API ...@@ -143,8 +143,8 @@ module API
success Entities::MergeRequest success Entities::MergeRequest
end end
params do params do
optional :title, type: String, desc: 'The title of the merge request' optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, desc: 'The target branch' optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen merge], optional :state_event, type: String, values: %w[close reopen merge],
desc: 'Status of the merge request' desc: 'Status of the merge request'
use :optional_params use :optional_params
......
module API module API
# Projects API
class Services < Grape::API class Services < Grape::API
services = {
'asana' => [
{
required: true,
name: :api_key,
type: String,
desc: 'User API token'
},
{
required: false,
name: :restrict_to_branch,
type: String,
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
}
],
'assembla' => [
{
required: true,
name: :token,
type: String,
desc: 'The authentication token'
},
{
required: false,
name: :subdomain,
type: String,
desc: 'Subdomain setting'
}
],
'bamboo' => [
{
required: true,
name: :bamboo_url,
type: String,
desc: 'Bamboo root URL like https://bamboo.example.com'
},
{
required: true,
name: :build_key,
type: String,
desc: 'Bamboo build plan key like'
},
{
required: true,
name: :username,
type: String,
desc: 'A user with API access, if applicable'
},
{
required: true,
name: :password,
type: String,
desc: 'Passord of the user'
}
],
'bugzilla' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'New issue URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'Issues URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'buildkite' => [
{
required: true,
name: :token,
type: String,
desc: 'Buildkite project GitLab token'
},
{
required: true,
name: :project_url,
type: String,
desc: 'The buildkite project URL'
},
{
required: false,
name: :enable_ssl_verification,
type: Boolean,
desc: 'Enable SSL verification for communication'
}
],
'builds-email' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :add_pusher,
type: Boolean,
desc: 'Add pusher to recipients list'
},
{
required: false,
name: :notify_only_broken_builds,
type: Boolean,
desc: 'Notify only broken builds'
}
],
'campfire' => [
{
required: true,
name: :token,
type: String,
desc: 'Campfire token'
},
{
required: false,
name: :subdomain,
type: String,
desc: 'Campfire subdomain'
},
{
required: false,
name: :room,
type: String,
desc: 'Campfire room'
},
],
'custom-issue-tracker' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'New issue URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'Issues URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'drone-ci' => [
{
required: true,
name: :token,
type: String,
desc: 'Drone CI token'
},
{
required: true,
name: :drone_url,
type: String,
desc: 'Drone CI URL'
},
{
required: false,
name: :enable_ssl_verification,
type: Boolean,
desc: 'Enable SSL verification for communication'
}
],
'emails-on-push' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :disable_diffs,
type: Boolean,
desc: 'Disable code diffs'
},
{
required: false,
name: :send_from_committer_email,
type: Boolean,
desc: 'Send from committer'
}
],
'external-wiki' => [
{
required: true,
name: :external_wiki_url,
type: String,
desc: 'The URL of the external Wiki'
}
],
'flowdock' => [
{
required: true,
name: :token,
type: String,
desc: 'Flowdock token'
}
],
'gemnasium' => [
{
required: true,
name: :api_key,
type: String,
desc: 'Your personal API key on gemnasium.com'
},
{
required: true,
name: :token,
type: String,
desc: "The project's slug on gemnasium.com"
}
],
'hipchat' => [
{
required: true,
name: :token,
type: String,
desc: 'The room token'
},
{
required: false,
name: :room,
type: String,
desc: 'The room name or ID'
},
{
required: false,
name: :color,
type: String,
desc: 'The room color'
},
{
required: false,
name: :notify,
type: Boolean,
desc: 'Enable notifications'
},
{
required: false,
name: :api_version,
type: String,
desc: 'Leave blank for default (v2)'
},
{
required: false,
name: :server,
type: String,
desc: 'Leave blank for default. https://hipchat.example.com'
}
],
'irker' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Recipients/channels separated by whitespaces'
},
{
required: false,
name: :default_irc_uri,
type: String,
desc: 'Default: irc://irc.network.net:6697'
},
{
required: false,
name: :server_host,
type: String,
desc: 'Server host. Default localhost'
},
{
required: false,
name: :server_port,
type: Integer,
desc: 'Server port. Default 6659'
},
{
required: false,
name: :colorize_messages,
type: Boolean,
desc: 'Colorize messages'
}
],
'jira' => [
{
required: true,
name: :url,
type: String,
desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
},
{
required: true,
name: :project_key,
type: String,
desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
},
{
required: false,
name: :username,
type: String,
desc: 'The username of the user created to be used with GitLab/JIRA'
},
{
required: false,
name: :password,
type: String,
desc: 'The password of the user created to be used with GitLab/JIRA'
},
{
required: false,
name: :jira_issue_transition_id,
type: Integer,
desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
}
],
'mattermost-slash-commands' => [
{
required: true,
name: :token,
type: String,
desc: 'The Mattermost token'
}
],
'pipelines-email' => [
{
required: true,
name: :recipients,
type: String,
desc: 'Comma-separated list of recipient email addresses'
},
{
required: false,
name: :notify_only_broken_builds,
type: Boolean,
desc: 'Notify only broken builds'
}
],
'pivotaltracker' => [
{
required: true,
name: :token,
type: String,
desc: 'The Pivotaltracker token'
},
{
required: false,
name: :restrict_to_branch,
type: String,
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
],
'pushover' => [
{
required: true,
name: :api_key,
type: String,
desc: 'The application key'
},
{
required: true,
name: :user_key,
type: String,
desc: 'The user key'
},
{
required: true,
name: :priority,
type: String,
desc: 'The priority'
},
{
required: true,
name: :device,
type: String,
desc: 'Leave blank for all active devices'
},
{
required: true,
name: :sound,
type: String,
desc: 'The sound of the notification'
}
],
'redmine' => [
{
required: true,
name: :new_issue_url,
type: String,
desc: 'The new issue URL'
},
{
required: true,
name: :project_url,
type: String,
desc: 'The project URL'
},
{
required: true,
name: :issues_url,
type: String,
desc: 'The issues URL'
},
{
required: false,
name: :description,
type: String,
desc: 'The description of the tracker'
}
],
'slack' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
},
{
required: false,
name: :new_issue_url,
type: String,
desc: 'The user name'
},
{
required: false,
name: :channel,
type: String,
desc: 'The channel name'
}
],
'teamcity' => [
{
required: true,
name: :teamcity_url,
type: String,
desc: 'TeamCity root URL like https://teamcity.example.com'
},
{
required: true,
name: :build_type,
type: String,
desc: 'Build configuration ID'
},
{
required: true,
name: :username,
type: String,
desc: 'A user with permissions to trigger a manual build'
},
{
required: true,
name: :password,
type: String,
desc: 'The password of the user'
}
]
}.freeze
trigger_services = {
'mattermost-slash-commands' => [
{
name: :token,
type: String,
desc: 'The Mattermost token'
}
]
}.freeze
resource :projects do resource :projects do
before { authenticate! } before { authenticate! }
before { authorize_admin_project } before { authorize_admin_project }
# Set <service_slug> service for project helpers do
# def service_attributes(service)
# Example Request: service.fields.inject([]) do |arr, hash|
# arr << hash[:name].to_sym
# PUT /projects/:id/services/gitlab-ci
#
put ':id/services/:service_slug' do
if project_service
validators = project_service.class.validators.select do |s|
s.class == ActiveRecord::Validations::PresenceValidator &&
s.attributes != [:project_id]
end end
end
end
required_attributes! validators.map(&:attributes).flatten.uniq services.each do |service_slug, settings|
attrs = attributes_for_keys service_attributes desc "Set #{service_slug} service for project"
params do
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
else
optional setting[:name], type: setting[:type], desc: setting[:desc]
end
end
end
put ":id/services/#{service_slug}" do
service = user_project.find_or_initialize_service(service_slug.underscore)
service_params = declared_params(include_missing: false).merge(active: true)
if project_service.update_attributes(attrs.merge(active: true)) if service.update_attributes(service_params)
true true
else else
not_found! render_api_error!('400 Bad Request', 400)
end end
end end
end end
# Delete <service_slug> service for project desc "Delete a service for project"
# params do
# Example Request: requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
# end
# DELETE /project/:id/services/gitlab-ci delete ":id/services/:service_slug" do
# service = user_project.find_or_initialize_service(params[:service_slug].underscore)
delete ':id/services/:service_slug' do
if project_service
attrs = service_attributes.inject({}) do |hash, key|
hash.merge!(key => nil)
end
if project_service.update_attributes(attrs.merge(active: false)) attrs = service_attributes(service).inject({}) do |hash, key|
true hash.merge!(key => nil)
else end
not_found!
end if service.update_attributes(attrs.merge(active: false))
true
else
render_api_error!('400 Bad Request', 400)
end end
end end
# Get <service_slug> service settings for project desc 'Get the service settings for project' do
# success Entities::ProjectService
# Example Request: end
# params do
# GET /project/:id/services/gitlab-ci requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
# end
get ':id/services/:service_slug' do get ":id/services/:service_slug" do
present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? service = user_project.find_or_initialize_service(params[:service_slug].underscore)
present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
end end
end end
resource :projects do trigger_services.each do |service_slug, settings|
desc 'Trigger a slash command' do params do
detail 'Added in GitLab 8.13' requires :id, type: String, desc: 'The ID of a project'
end end
post ':id/services/:service_slug/trigger' do resource :projects do
project = find_project(params[:id]) desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
params do
settings.each do |setting|
requires setting[:name], type: setting[:type], desc: setting[:desc]
end
end
post ":id/services/#{service_slug.underscore}/trigger" do
project = find_project(params[:id])
# This is not accurate, but done to prevent leakage of the project names # This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project not_found!('Service') unless project
service = project_service(project) service = project.find_or_initialize_service(service_slug.underscore)
result = service.try(:active?) && service.try(:trigger, params) result = service.try(:active?) && service.try(:trigger, params)
if result if result
status result[:status] || 200 status result[:status] || 200
present result present result
else else
not_found!('Service') not_found!('Service')
end
end end
end end
end end
......
module API
# Snippets API
class Snippets < Grape::API
include PaginationParams
before { authenticate! }
resource :snippets do
helpers do
def snippets_for_current_user
SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
end
def public_snippets
SnippetsFinder.new.execute(current_user, filter: :public)
end
end
desc 'Get a snippets list for authenticated user' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
use :pagination
end
get do
present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
end
desc 'List all public snippets current_user has access to' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
use :pagination
end
get 'public' do
present paginate(public_snippets), with: Entities::PersonalSnippet
end
desc 'Get a single snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
get ':id' do
snippet = snippets_for_current_user.find(params[:id])
present snippet, with: Entities::PersonalSnippet
end
desc 'Create new snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :title, type: String, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
requires :content, type: String, desc: 'The content of a snippet'
optional :visibility_level, type: Integer,
values: Gitlab::VisibilityLevel.values,
default: Gitlab::VisibilityLevel::INTERNAL,
desc: 'The visibility level of the snippet'
end
post do
attrs = declared_params(include_missing: false)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
render_validation_error!(snippet)
end
end
desc 'Update an existing snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
optional :title, type: String, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, desc: 'The content of a snippet'
optional :visibility_level, type: Integer,
values: Gitlab::VisibilityLevel.values,
desc: 'The visibility level of the snippet'
at_least_one_of :title, :file_name, :content, :visibility_level
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false)
UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
render_validation_error!(snippet)
end
end
desc 'Remove snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
snippet.destroy
no_content!
end
desc 'Get a raw snippet' do
detail 'This feature was introduced in GitLab 8.15.'
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
present snippet.content
end
end
end
end
...@@ -353,7 +353,7 @@ module API ...@@ -353,7 +353,7 @@ module API
success Entities::UserPublic success Entities::UserPublic
end end
get do get do
present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
end end
desc "Get the currently authenticated user's SSH keys" do desc "Get the currently authenticated user's SSH keys" do
......
...@@ -248,7 +248,7 @@ module Banzai ...@@ -248,7 +248,7 @@ module Banzai
end end
def projects_relation_for_paths(paths) def projects_relation_for_paths(paths)
Project.where_paths_in(paths).includes(:namespace) Project.where_full_path_in(paths).includes(:namespace)
end end
# Returns projects for the given paths. # Returns projects for the given paths.
......
...@@ -46,7 +46,7 @@ module Banzai ...@@ -46,7 +46,7 @@ module Banzai
end end
def rebuild_relative_uri(uri) def rebuild_relative_uri(uri)
file_path = relative_file_path(uri.path) file_path = relative_file_path(uri)
uri.path = [ uri.path = [
relative_url_root, relative_url_root,
...@@ -59,8 +59,10 @@ module Banzai ...@@ -59,8 +59,10 @@ module Banzai
uri uri
end end
def relative_file_path(path) def relative_file_path(uri)
nested_path = build_relative_path(path, context[:requested_path]) path = Addressable::URI.unescape(uri.path)
request_path = Addressable::URI.unescape(context[:requested_path])
nested_path = build_relative_path(path, request_path)
file_exists?(nested_path) ? nested_path : path file_exists?(nested_path) ? nested_path : path
end end
...@@ -108,11 +110,7 @@ module Banzai ...@@ -108,11 +110,7 @@ module Banzai
end end
def uri_type(path) def uri_type(path)
@uri_types[path] ||= begin @uri_types[path] ||= current_commit.uri_type(path)
unescaped_path = Addressable::URI.unescape(path)
current_commit.uri_type(unescaped_path)
end
end end
def current_commit def current_commit
......
...@@ -61,7 +61,7 @@ module Gitlab ...@@ -61,7 +61,7 @@ module Gitlab
end end
def file_name_regex def file_name_regex
@file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
end end
def file_name_regex_message def file_name_regex_message
...@@ -69,7 +69,7 @@ module Gitlab ...@@ -69,7 +69,7 @@ module Gitlab
end end
def file_path_regex def file_path_regex
@file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze
end end
def file_path_regex_message def file_path_regex_message
......
...@@ -24,6 +24,8 @@ module Gitlab ...@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url wiki_page_url
when ProjectSnippet when ProjectSnippet
project_snippet_url(object) project_snippet_url(object)
when Snippet
personal_snippet_url(object)
else else
raise NotImplementedError.new("No URL builder defined for #{object.class}") raise NotImplementedError.new("No URL builder defined for #{object.class}")
end end
......
require Rails.root.join('lib/gitlab/database')
require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
desc 'GitLab | Sets up PostgreSQL' desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
end end
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"eslint": "eslint --ext .js,.js.es6 .", "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
"eslint-fix": "npm run eslint -- --fix", "eslint-fix": "npm run eslint -- --fix",
"eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
}, },
......
require 'spec_helper'
describe Projects::ReleasesController do
let!(:project) { create(:project) }
let!(:user) { create(:user) }
let!(:release) { create(:release, project: project) }
let!(:tag) { release.tag }
before do
project.team << [user, :developer]
sign_in(user)
end
describe 'GET #edit' do
it 'initializes a new release' do
tag_id = release.tag
project.releases.destroy_all
get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
release = assigns(:release)
expect(release).not_to be_nil
expect(release).not_to be_persisted
end
it 'retrieves an existing release' do
get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
release = assigns(:release)
expect(release).not_to be_nil
expect(release).to be_persisted
end
end
describe 'PUT #update' do
it 'updates release note description' do
update_release('description updated')
release = project.releases.find_by_tag(tag)
expect(release.description).to eq("description updated")
end
it 'deletes release note when description is null' do
expect { update_release('') }.to change(project.releases, :count).by(-1)
end
end
def update_release(description)
put :update,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
tag_id: release.tag,
release: { description: description }
end
end
...@@ -19,5 +19,9 @@ FactoryGirl.define do ...@@ -19,5 +19,9 @@ FactoryGirl.define do
trait :access_requestable do trait :access_requestable do
request_access_enabled true request_access_enabled true
end end
trait :nested do
parent factory: :group
end
end end
end end
class Spinach::Features::AdminSettings < Spinach::FeatureSteps require 'spec_helper'
include SharedAuthentication
include SharedPaths
include SharedAdmin
include Gitlab::CurrentSettings
step 'I modify settings and save form' do feature 'Admin updates settings', feature: true do
before(:each) do
login_as :admin
visit admin_application_settings_path
end
scenario 'Change application settings' do
uncheck 'Gravatar enabled' uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/' fill_in 'Home page URL', with: 'https://about.gitlab.com/'
fill_in 'Help page text', with: 'Example text' fill_in 'Help page text', with: 'Example text'
click_button 'Save' click_button 'Save'
end
step 'I should see application settings saved' do
expect(current_application_settings.gravatar_enabled).to be_falsey expect(current_application_settings.gravatar_enabled).to be_falsey
expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
step 'I click on "Service Templates"' do scenario 'Change Slack Service template settings' do
click_link 'Service Templates' click_link 'Service Templates'
end
step 'I click on "Slack" service' do
click_link 'Slack' click_link 'Slack'
end
step 'I check all events and submit form' do
page.check('Active')
page.check('Push')
page.check('Tag push')
page.check('Note')
page.check('Issue')
page.check('Merge request')
page.check('Build')
page.check('Pipeline')
click_on 'Save'
end
step 'I fill out Slack settings' do
fill_in 'Webhook', with: 'http://localhost' fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user' fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel' fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken builds') page.check('Notify only broken builds')
end
step 'I should see service template settings saved' do check_all_events
click_on 'Save'
expect(page).to have_content 'Application settings saved successfully' expect(page).to have_content 'Application settings saved successfully'
end
step 'I should see all checkboxes checked' do click_link 'Slack'
page.all('input[type=checkbox]').each do |checkbox| page.all('input[type=checkbox]').each do |checkbox|
expect(checkbox).to be_checked expect(checkbox).to be_checked
end end
end
step 'I should see Slack settings saved' do
expect(find_field('Webhook').value).to eq 'http://localhost' expect(find_field('Webhook').value).to eq 'http://localhost'
expect(find_field('Username').value).to eq 'test_user' expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel' expect(find('#service_push_channel').value).to eq '#test_channel'
end end
def check_all_events
page.check('Active')
page.check('Push')
page.check('Tag push')
page.check('Note')
page.check('Issue')
page.check('Merge request')
page.check('Build')
page.check('Pipeline')
end
end end
...@@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do ...@@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do
visit user_snippets_path(user) visit user_snippets_path(user)
wait_for_ajax() wait_for_ajax()
page.find('.js-timeago').hover page.find('.js-timeago.snippet-created-ago').hover
end end
it 'has the datetime formated correctly' do it 'has the datetime formated correctly' do
......
...@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do ...@@ -10,7 +10,7 @@ feature 'Groups > Members > Last owner cannot leave group', feature: true do
visit group_path(group) visit group_path(group)
end end
scenario 'user does not see a "Leave Group" link' do scenario 'user does not see a "Leave group" link' do
expect(page).not_to have_content 'Leave Group' expect(page).not_to have_content 'Leave group'
end end
end end
...@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do ...@@ -13,7 +13,7 @@ feature 'Groups > Members > Member leaves group', feature: true do
end end
scenario 'user leaves group' do scenario 'user leaves group' do
click_link 'Leave Group' click_link 'Leave group'
expect(current_path).to eq(dashboard_groups_path) expect(current_path).to eq(dashboard_groups_path)
expect(group.users.exists?(user.id)).to be_falsey expect(group.users.exists?(user.id)).to be_falsey
......
...@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do ...@@ -29,7 +29,7 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.' expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request' expect(page).to have_content 'Withdraw Access Request'
expect(page).not_to have_content 'Leave Group' expect(page).not_to have_content 'Leave group'
end end
scenario 'user does not see private projects' do scenario 'user does not see private projects' do
......
require 'spec_helper' require 'spec_helper'
describe 'Help Pages', feature: true do describe 'Help Pages', feature: true do
describe 'Show SSH page' do
before do
login_as :user
end
it 'replaces the variable $your_email with the email of the user' do
visit help_page_path('ssh/README')
expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
end
end
describe 'Get the main help page' do describe 'Get the main help page' do
shared_examples_for 'help page' do |prefix: ''| shared_examples_for 'help page' do |prefix: ''|
it 'prefixes links correctly' do it 'prefixes links correctly' do
......
...@@ -2,8 +2,9 @@ require 'rails_helper' ...@@ -2,8 +2,9 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user, username: 'someone.special') }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
before do before do
...@@ -23,21 +24,69 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -23,21 +24,69 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).to have_selector('.atwho-container') expect(page).to have_selector('.atwho-container')
end end
it 'opens autocomplete menu when field is prefixed with non-text character' do it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note_note').native.send_keys('') find('#note_note').native.send_keys('testing')
find('#note_note').native.send_keys('@') find('#note_note').native.send_keys('@')
end end
expect(page).to have_selector('.atwho-container') expect(page).not_to have_selector('.atwho-view')
end end
it 'doesnt open autocomplete menu character is prefixed with text' do context 'if a selected value has special characters' do
page.within '.timeline-content-form' do it 'wraps the result in double quotes' do
find('#note_note').native.send_keys('testing') note = find('#note_note')
find('#note_note').native.send_keys('@') page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
sleep 1
note.click
end
label_item = find('.atwho-view li', text: label.title)
expect_to_wrap(true, label_item, note, label.title)
end end
expect(page).not_to have_selector('.atwho-view') it 'doesn\'t wrap for assignee values' do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
sleep 1
note.click
end
user_item = find('.atwho-view li', text: user.username)
expect_to_wrap(false, user_item, note, user.username)
end
it 'doesn\'t wrap for emoji values' do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
sleep 1
note.click
end
emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
item.click
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
end
end end
end end
require 'spec_helper'
feature 'User wants to create a file', feature: true do
include WaitForAjax
let(:project) { create(:project) }
let(:user) { create(:user) }
background do
project.team << [user, :master]
login_as user
visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
end
def submit_new_file(options)
file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md'
file_content = find('#file-content')
file_content.set options[:file_content] || 'Some content'
click_button 'Commit Changes'
end
scenario 'file name contains Chinese characters' do
submit_new_file(file_name: '测试.md')
expect(page).to have_content 'The file has been successfully created.'
end
scenario 'directory name contains Chinese characters' do
submit_new_file(file_name: '中文/测试.md')
expect(page).to have_content 'The file has been successfully created.'
end
scenario 'file name contains invalid characters' do
submit_new_file(file_name: '\\')
expect(page).to have_content 'Your changes could not be committed, because the file name can contain only'
end
scenario 'file name contains directory traversal' do
submit_new_file(file_name: '../README.md')
expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.'
end
end
...@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature: ...@@ -12,6 +12,6 @@ feature 'Projects > Members > Group member cannot leave group project', feature:
end end
scenario 'user does not see a "Leave project" link' do scenario 'user does not see a "Leave project" link' do
expect(page).not_to have_content 'Leave Project' expect(page).not_to have_content 'Leave project'
end end
end end
require 'spec_helper' require 'spec_helper'
feature 'Projects > Members > Group requester cannot request access to project', feature: true do feature 'Projects > Members > Group requester cannot request access to project', feature: true, js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:owner) { create(:user) } let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) } let(:group) { create(:group, :public, :access_requestable) }
......
...@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do ...@@ -11,7 +11,7 @@ feature 'Projects > Members > Member leaves project', feature: true do
end end
scenario 'user leaves project' do scenario 'user leaves project' do
click_link 'Leave Project' click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path) expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey expect(project.users.exists?(user.id)).to be_falsey
......
...@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do ...@@ -8,7 +8,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
end end
scenario 'user does not see a "Leave Project" link' do scenario 'user does not see a "Leave project" link' do
expect(page).not_to have_content 'Leave Project' expect(page).not_to have_content 'Leave project'
end end
end end
...@@ -9,65 +9,74 @@ describe SnippetsFinder do ...@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) } let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do context ':all filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public)
end
it "returns all private and internal snippets" do it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all) snippets = SnippetsFinder.new.execute(user, filter: :all)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns all public snippets" do it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all) snippets = SnippetsFinder.new.execute(nil, filter: :all)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
end end
context ':by_user filter' do context ':public filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private, author: user) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal, author: user) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public, author: user)
it "returns public public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :public)
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end end
end
context ':by_user filter' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns internal snippets" do it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
expect(snippets).to include(@snippet2) expect(snippets).to include(snippet2)
expect(snippets).not_to include(@snippet1, @snippet3) expect(snippets).not_to include(snippet1, snippet3)
end end
it "returns private snippets" do it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
expect(snippets).to include(@snippet1) expect(snippets).to include(snippet1)
expect(snippets).not_to include(@snippet2, @snippet3) expect(snippets).not_to include(snippet2, snippet3)
end end
it "returns public snippets" do it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
it "returns all snippets" do it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
expect(snippets).to include(@snippet1, @snippet2, @snippet3) expect(snippets).to include(snippet1, snippet2, snippet3)
end end
it "returns only public snippets if unauthenticated user" do it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet2, @snippet1) expect(snippets).not_to include(snippet2, snippet1)
end end
end end
...@@ -84,16 +93,39 @@ describe SnippetsFinder do ...@@ -84,16 +93,39 @@ describe SnippetsFinder do
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(@snippet1, @snippet2)
end end
it "returns public and internal snippets for none project members" do it "returns public and internal snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(@snippet1)
end end
it "returns public snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
it "returns all snippets for project members" do it "returns all snippets for project members" do
project1.team << [user, :developer] project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3) expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end end
it "returns private snippets for project members" do
project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
expect(snippets).to include(@snippet1)
end
end end
end end
...@@ -33,9 +33,9 @@ ...@@ -33,9 +33,9 @@
}; };
describe('AwardsHandler', function() { describe('AwardsHandler', function() {
fixture.preload('awards_handler.html'); fixture.preload('issues/open-issue.html.raw');
beforeEach(function() { beforeEach(function() {
fixture.load('awards_handler.html'); fixture.load('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler; awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) { return function(url, emoji, cb) {
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
}); });
describe('::getAwardUrl', function() { describe('::getAwardUrl', function() {
return it('should return the url for request', function() { return it('should return the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
}); });
}); });
describe('::addAward and ::checkMutuality', function() { describe('::addAward and ::checkMutuality', function() {
...@@ -209,7 +209,7 @@ ...@@ -209,7 +209,7 @@
$('.js-add-award').eq(0).click(); $('.js-add-award').eq(0).click();
$menu = $('.emoji-menu'); $menu = $('.emoji-menu');
$block = $('.js-awards-block'); $block = $('.js-awards-block');
$emoji = $menu.find(".emoji-menu-list-item " + selector); $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
expect($emoji.length).toBe(1); expect($emoji.length).toBe(1);
expect($block.find(selector).length).toBe(0); expect($block.find(selector).length).toBe(0);
$emoji.click(); $emoji.click();
...@@ -224,7 +224,7 @@ ...@@ -224,7 +224,7 @@
openEmojiMenuAndAddEmoji(); openEmojiMenuAndAddEmoji();
$('.js-add-award').eq(0).click(); $('.js-add-award').eq(0).click();
$block = $('.js-awards-block'); $block = $('.js-awards-block');
$emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
$emoji.click(); $emoji.click();
return expect($block.find(selector).length).toBe(0); return expect($block.find(selector).length).toBe(0);
}); });
......
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
.description.js-task-list-container.is-task-list-enabled
.wiki
%p Qui exercitationem magnam optio quae fuga earum odio.
%textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
%small.edited-text
.content-block.content-block-small
.awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
%span.award-control-text.js-counter 0
%button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
.icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
%span.award-control-text.js-counter 0
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%section.issuable-discussion
#notes
%ul#notes-list.notes.main-notes-list.timeline
%li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
.timeline-entry-inner
.timeline-icon
%a{:href => "/u/agustin"}
%img.avatar.s40{:alt => "", :src => "#"}/
.timeline-content
.note-header
%a.author_link{:href => "/u/agustin"}
%span.author Brenna Stokes
.inline.note-headline-light
@agustin commented
%a{:href => "#note_348"}
%time 11 days ago
.note-actions
%span.note-role Reporter
%a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
%i.fa.fa-spinner.fa-spin
%i.fa.fa-smile-o.link-highlight
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
.note-awards
.awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{:type => "button"}
%i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
%i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
%span.award-control-text Add
%div.pipeline-visualization.js-pipeline-graph
%ul.stage-column-list
%li.stage-column
.stage-name
%a{:href => "/"}
Test
.builds-container
%ul
%li.build
.curve
.build-content
%a
%svg
.ci-status-text
stop_review
...@@ -106,6 +106,18 @@ ...@@ -106,6 +106,18 @@
}); });
}); });
describe('mergeInProgress', function() {
it('should display error with h4 tag', function() {
spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
});
spyOn($, 'ajax').and.callFake(function(e) {
e.success({ merge_error: 'Sorry, something went wrong.' });
});
this.class.mergeInProgress(null);
});
});
return describe('getCIStatus', function() { return describe('getCIStatus', function() {
beforeEach(function() { beforeEach(function() {
this.ciStatusData = { this.ciStatusData = {
......
//= require pipelines
(() => {
describe('Pipelines', () => {
fixture.preload('pipeline_graph');
beforeEach(() => {
fixture.load('pipeline_graph');
});
it('should be defined', () => {
expect(window.gl.Pipelines).toBeDefined();
});
it('should create a `Pipelines` instance without options', () => {
expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
});
it('should create a `Pipelines` instance with options', () => {
const pipelines = new window.gl.Pipelines({ foo: 'bar' });
expect(pipelines.pipelineGraph).toBeDefined();
});
});
})();
...@@ -21,16 +21,18 @@ ...@@ -21,16 +21,18 @@
return this.project = new Project(); return this.project = new Project();
}); });
return describe('project list', function() { return describe('project list', function() {
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
d = $.Deferred();
d.resolve(this.projects_data);
return d.promise();
};
beforeEach((function(_this) { beforeEach((function(_this) {
return function() { return function() {
_this.projects_data = fixture.load('projects.json')[0]; _this.projects_data = fixture.load('projects.json')[0];
return spyOn(jQuery, 'ajax').and.callFake(function(req) { return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
d = $.Deferred();
d.resolve(_this.projects_data);
return d.promise();
});
}; };
})(this)); })(this));
it('to show on toggle click', (function(_this) { it('to show on toggle click', (function(_this) {
......
...@@ -14,8 +14,9 @@ ...@@ -14,8 +14,9 @@
startingInterval: DEFAULT_STARTING_INTERVAL, startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
delayStartBy: 0,
lazyStart: false, lazyStart: false,
immediateExecution: false,
hiddenInterval: null,
}; };
if (config) { if (config) {
...@@ -114,14 +115,31 @@ ...@@ -114,14 +115,31 @@
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden'; interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should change to the hidden interval when page is not visible', function (done) {
const HIDDEN_INTERVAL = 1500;
const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
// simulates triggering of visibilitychange event
interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should resume when page is becomes visible at the previous interval', function (done) { it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval; const interval = this.smartInterval;
...@@ -129,14 +147,12 @@ ...@@ -129,14 +147,12 @@
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden'; interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeUndefined(); expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event // simulates triggering of visibilitychange event
interval.state.pageVisibility = 'visible'; interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeTruthy(); expect(interval.state.intervalId).toBeTruthy();
...@@ -154,6 +170,11 @@ ...@@ -154,6 +170,11 @@
done(); done();
}, DEFAULT_SHORT_TIMEOUT); }, DEFAULT_SHORT_TIMEOUT);
}); });
it('should execute callback before first interval', function () {
const interval = createDefaultSmartInterval({ immediateExecution: true });
expect(interval.cfg.immediateExecution).toBeFalsy();
});
}); });
}); });
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do ...@@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped)) doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match '/raw/' expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}"
end end
context 'when requested path is a file in the repo' do context 'when requested path is a file in the repo' do
......
...@@ -3,6 +3,10 @@ require 'spec_helper' ...@@ -3,6 +3,10 @@ require 'spec_helper'
describe Group, 'Routable' do describe Group, 'Routable' do
let!(:group) { create(:group) } let!(:group) { create(:group) }
describe 'Validations' do
it { is_expected.to validate_presence_of(:route) }
end
describe 'Associations' do describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) } it { is_expected.to have_one(:route).dependent(:destroy) }
end end
...@@ -35,16 +39,16 @@ describe Group, 'Routable' do ...@@ -35,16 +39,16 @@ describe Group, 'Routable' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end end
describe '.where_paths_in' do describe '.where_full_path_in' do
context 'without any paths' do context 'without any paths' do
it 'returns an empty relation' do it 'returns an empty relation' do
expect(described_class.where_paths_in([])).to eq([]) expect(described_class.where_full_path_in([])).to eq([])
end end
end end
context 'without any valid paths' do context 'without any valid paths' do
it 'returns an empty relation' do it 'returns an empty relation' do
expect(described_class.where_paths_in(%w[unknown])).to eq([]) expect(described_class.where_full_path_in(%w[unknown])).to eq([])
end end
end end
...@@ -52,13 +56,13 @@ describe Group, 'Routable' do ...@@ -52,13 +56,13 @@ describe Group, 'Routable' do
let!(:nested_group) { create(:group, parent: group) } let!(:nested_group) { create(:group, parent: group) }
it 'returns the projects matching the paths' do it 'returns the projects matching the paths' do
result = described_class.where_paths_in([group.to_param, nested_group.to_param]) result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
expect(result).to contain_exactly(group, nested_group) expect(result).to contain_exactly(group, nested_group)
end end
it 'returns projects regardless of the casing of paths' do it 'returns projects regardless of the casing of paths' do
result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase]) result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase])
expect(result).to contain_exactly(group, nested_group) expect(result).to contain_exactly(group, nested_group)
end end
......
...@@ -271,4 +271,11 @@ describe Group, models: true do ...@@ -271,4 +271,11 @@ describe Group, models: true do
expect(group.web_url).to include("groups/#{group.name}") expect(group.web_url).to include("groups/#{group.name}")
end end
end end
describe 'nested group' do
subject { create(:group, :nested) }
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) }
end
end end
...@@ -727,17 +727,6 @@ describe User, models: true do ...@@ -727,17 +727,6 @@ describe User, models: true do
end end
end end
describe 'by_username_or_id' do
let(:user1) { create(:user, username: 'foo') }
it "gets the correct user" do
expect(User.by_username_or_id(user1.id)).to eq(user1)
expect(User.by_username_or_id('foo')).to eq(user1)
expect(User.by_username_or_id(-1)).to be_nil
expect(User.by_username_or_id('bar')).to be_nil
end
end
describe '.find_by_ssh_key_id' do describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do context 'using an existing SSH key ID' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -11,6 +11,7 @@ describe API::Branches, api: true do ...@@ -11,6 +11,7 @@ describe API::Branches, api: true do
let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' } let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do it "returns an array of project branches" do
...@@ -37,6 +38,13 @@ describe API::Branches, api: true do ...@@ -37,6 +38,13 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false) expect(json_response['developers_can_merge']).to eq(false)
end end
it "returns the branch information for a single branch with dots in the name" do
get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq("with.1.2.3")
end
context 'on a merged branch' do context 'on a merged branch' do
it "returns the branch information for a single branch" do it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/merge-test", user) get api("/projects/#{project.id}/repository/branches/merge-test", user)
...@@ -71,6 +79,14 @@ describe API::Branches, api: true do ...@@ -71,6 +79,14 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false) expect(json_response['developers_can_merge']).to eq(false)
end end
it "protects a single branch with dots in the name" do
put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq("with.1.2.3")
expect(json_response['protected']).to eq(true)
end
it 'protects a single branch and developers can push' do it 'protects a single branch and developers can push' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
developers_can_push: true developers_can_push: true
...@@ -220,6 +236,14 @@ describe API::Branches, api: true do ...@@ -220,6 +236,14 @@ describe API::Branches, api: true do
expect(json_response['protected']).to eq(false) expect(json_response['protected']).to eq(false)
end end
it "update branches with dots in branch name" do
put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq("with.1.2.3")
expect(json_response['protected']).to eq(false)
end
it "returns success when unprotect branch" do it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
...@@ -292,6 +316,13 @@ describe API::Branches, api: true do ...@@ -292,6 +316,13 @@ describe API::Branches, api: true do
expect(json_response['branch_name']).to eq(branch_name) expect(json_response['branch_name']).to eq(branch_name)
end end
it "removes a branch with dots in the branch name" do
delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
expect(response).to have_http_status(200)
expect(json_response['branch_name']).to eq("with.1.2.3")
end
it 'returns 404 if branch not exists' do it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user) delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
...@@ -456,6 +456,76 @@ describe API::Commits, api: true do ...@@ -456,6 +456,76 @@ describe API::Commits, api: true do
end end
end end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
context 'authorized user' do
it 'cherry picks a commit' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq(master_pickable_commit.title)
expect(json_response['message']).to eq(master_pickable_commit.message)
expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
expect(json_response['committer_name']).to eq(user.name)
end
it 'returns 400 if commit is already included in the target branch' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
It may have already been cherry-pick, or a more recent commit may have updated some of its content.')
end
it 'returns 400 if you are not allowed to push to the target branch' do
project.team << [user2, :developer]
protected_branch = create(:protected_branch, project: project, name: 'feature')
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('You are not allowed to push into this branch')
end
it 'returns 400 for missing parameters' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('branch is missing')
end
it 'returns 404 if commit is not found' do
post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Commit Not Found')
end
it 'returns 404 if branch is not found' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Branch Not Found')
end
it 'returns 400 for missing parameters' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('branch is missing')
end
end
context 'unauthorized user' do
it 'does not cherry pick the commit' do
post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
expect(response).to have_http_status(401)
end
end
end
describe 'Post comment to commit' do describe 'Post comment to commit' do
context 'authorized user' do context 'authorized user' do
it 'returns comment' do it 'returns comment' do
......
...@@ -243,6 +243,28 @@ describe API::Groups, api: true do ...@@ -243,6 +243,28 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2) expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] } project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name]) expect(project_names).to match_array([project1.name, project3.name])
expect(json_response.first['default_branch']).to be_present
end
it "returns the group's projects with simple representation" do
get api("/groups/#{group1.id}/projects", user1), simple: true
expect(response).to have_http_status(200)
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
expect(json_response.first['default_branch']).not_to be_present
end
it 'filters the groups projects' do
public_project = create(:project, :public, path: 'test1', group: group1)
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
expect(response).to have_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(public_project.name)
end end
it "does not return a non existing group" do it "does not return a non existing group" do
......
...@@ -2,7 +2,6 @@ require 'spec_helper' ...@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Helpers, api: true do describe API::Helpers, api: true do
include API::Helpers include API::Helpers
include ApiHelpers
include SentryHelper include SentryHelper
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -13,18 +12,18 @@ describe API::Helpers, api: true do ...@@ -13,18 +12,18 @@ describe API::Helpers, api: true do
let(:env) { { 'REQUEST_METHOD' => 'GET' } } let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) } let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier) def set_env(user_or_token, identifier)
clear_env clear_env
clear_param clear_param
env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
env[API::Helpers::SUDO_HEADER] = identifier env[API::Helpers::SUDO_HEADER] = identifier.to_s
end end
def set_param(token_usr, identifier) def set_param(user_or_token, identifier)
clear_env clear_env
clear_param clear_param
params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
params[API::Helpers::SUDO_PARAM] = identifier params[API::Helpers::SUDO_PARAM] = identifier.to_s
end end
def clear_env def clear_env
...@@ -163,6 +162,13 @@ describe API::Helpers, api: true do ...@@ -163,6 +162,13 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user) expect(current_user).to eq(user)
end end
it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
set_env(admin, user.id)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do it 'handles sudo to oneself' do
set_env(admin, admin.id) set_env(admin, admin.id)
...@@ -294,33 +300,48 @@ describe API::Helpers, api: true do ...@@ -294,33 +300,48 @@ describe API::Helpers, api: true do
end end
end end
describe '.sudo_identifier' do describe '.sudo?' do
it "returns integers when input is an int" do context 'when no sudo env or param is passed' do
set_env(admin, '123') before do
expect(sudo_identifier).to eq(123) doorkeeper_guard_returns(nil)
set_env(admin, '0001234567890') end
expect(sudo_identifier).to eq(1234567890)
it 'returns false' do
set_param(admin, '123') expect(sudo?).to be_falsy
expect(sudo_identifier).to eq(123) end
set_param(admin, '0001234567890')
expect(sudo_identifier).to eq(1234567890)
end end
it "returns string when input is an is not an int" do context 'when sudo env or param is passed', 'user is not an admin' do
set_env(admin, '12.30') before do
expect(sudo_identifier).to eq("12.30") set_env(user, '123')
set_env(admin, 'hello') end
expect(sudo_identifier).to eq('hello')
set_env(admin, ' 123') it 'returns an 403 Forbidden' do
expect(sudo_identifier).to eq(' 123') expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
end
set_param(admin, '12.30') end
expect(sudo_identifier).to eq("12.30")
set_param(admin, 'hello') context 'when sudo env or param is passed', 'user is admin' do
expect(sudo_identifier).to eq('hello') context 'personal access token is used' do
set_param(admin, ' 123') before do
expect(sudo_identifier).to eq(' 123') personal_access_token = create(:personal_access_token, user: admin)
set_env(personal_access_token.token, user.id)
end
it 'returns an 403 Forbidden' do
expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
end
end
context 'private access token is used' do
before do
set_env(admin.private_token, user.id)
end
it 'returns true' do
expect(sudo?).to be_truthy
end
end
end end
end end
......
...@@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do ...@@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do
expect(json_response['labels']).to include '?' expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&' expect(json_response['labels']).to include '&'
end end
it 'does not update state when title is empty' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
merge_request.reload
expect(response).to have_http_status(400)
expect(merge_request.state).to eq('opened')
end
it 'does not update state when target_branch is empty' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
merge_request.reload
expect(response).to have_http_status(400)
expect(merge_request.state).to eq('opened')
end
end end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
......
...@@ -2,6 +2,7 @@ require "spec_helper" ...@@ -2,6 +2,7 @@ require "spec_helper"
describe API::Services, api: true do describe API::Services, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
...@@ -98,7 +99,7 @@ describe API::Services, api: true do ...@@ -98,7 +99,7 @@ describe API::Services, api: true do
post api("/projects/#{project.id}/services/idonotexist/trigger") post api("/projects/#{project.id}/services/idonotexist/trigger")
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
expect(json_response["message"]).to eq("404 Service Not Found") expect(json_response["error"]).to eq("404 Not Found")
end end
end end
...@@ -114,7 +115,7 @@ describe API::Services, api: true do ...@@ -114,7 +115,7 @@ describe API::Services, api: true do
end end
it 'when the service is inactive' do it 'when the service is inactive' do
post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger") post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
......
require 'rails_helper'
describe API::Snippets, api: true do
include ApiHelpers
let!(:user) { create(:user) }
describe 'GET /snippets/' do
it 'returns snippets available' do
public_snippet = create(:personal_snippet, :public, author: user)
private_snippet = create(:personal_snippet, :private, author: user)
internal_snippet = create(:personal_snippet, :internal, author: user)
get api("/snippets/", user)
expect(response).to have_http_status(200)
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
internal_snippet.id,
private_snippet.id)
expect(json_response.last).to have_key('web_url')
expect(json_response.last).to have_key('raw_url')
end
it 'hides private snippets from regular user' do
create(:personal_snippet, :private)
get api("/snippets/", user)
expect(response).to have_http_status(200)
expect(json_response.size).to eq(0)
end
end
describe 'GET /snippets/public' do
let!(:other_user) { create(:user) }
let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
it 'returns all snippets with public visibility from all users' do
get api("/snippets/public", user)
expect(response).to have_http_status(200)
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}/raw",
"http://localhost/snippets/#{public_snippet_other.id}/raw")
end
end
describe 'GET /snippets/:id/raw' do
let(:snippet) { create(:personal_snippet, author: user) }
it 'returns raw text' do
get api("/snippets/#{snippet.id}/raw", user)
expect(response).to have_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
it 'returns 404 for invalid snippet id' do
delete api("/snippets/1234", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
describe 'POST /snippets/' do
let(:params) do
{
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
it 'creates a new snippet' do
expect do
post api("/snippets/", user), params
end.to change { PersonalSnippet.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['title']).to eq(params[:title])
expect(json_response['file_name']).to eq(params[:file_name])
end
it 'returns 400 for missing parameters' do
params.delete(:title)
post api("/snippets/", user), params
expect(response).to have_http_status(400)
end
end
describe 'PUT /snippets/:id' do
let(:other_user) { create(:user) }
let(:public_snippet) { create(:personal_snippet, :public, author: user) }
it 'updates snippet' do
new_content = 'New content'
put api("/snippets/#{public_snippet.id}", user), content: new_content
expect(response).to have_http_status(200)
public_snippet.reload
expect(public_snippet.content).to eq(new_content)
end
it 'returns 404 for invalid snippet id' do
put api("/snippets/1234", user), title: 'foo'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it "returns 404 for another user's snippet" do
put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put api("/snippets/1234", user)
expect(response).to have_http_status(400)
end
end
describe 'DELETE /snippets/:id' do
let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
it 'deletes snippet' do
expect do
delete api("/snippets/#{public_snippet.id}", user)
expect(response).to have_http_status(204)
end.to change { PersonalSnippet.count }.by(-1)
end
it 'returns 404 for invalid snippet id' do
delete api("/snippets/1234", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
end
...@@ -651,13 +651,12 @@ describe API::Users, api: true do ...@@ -651,13 +651,12 @@ describe API::Users, api: true do
end end
describe "GET /user" do describe "GET /user" do
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user).token }
let(:private_token) { user.private_token }
context 'with regular user' do context 'with regular user' do
context 'with personal access token' do context 'with personal access token' do
it 'returns 403 without private token when sudo is defined' do it 'returns 403 without private token when sudo is defined' do
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") get api("/user?private_token=#{personal_access_token}&sudo=123")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
...@@ -665,7 +664,7 @@ describe API::Users, api: true do ...@@ -665,7 +664,7 @@ describe API::Users, api: true do
context 'with private token' do context 'with private token' do
it 'returns 403 without private token when sudo defined' do it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}") get api("/user?private_token=#{user.private_token}&sudo=123")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
...@@ -676,40 +675,44 @@ describe API::Users, api: true do ...@@ -676,40 +675,44 @@ describe API::Users, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(user.id)
end end
end end
context 'with admin' do context 'with admin' do
let(:user) { create(:admin) } let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
context 'with personal access token' do context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
it 'returns current user without private token when sudo not defined' do it 'returns initial current user without private token when sudo not defined' do
get api("/user?private_token=#{personal_access_token.token}") get api("/user?private_token=#{admin_personal_access_token}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(admin.id)
end end
end end
context 'with private token' do context 'with private token' do
it 'returns current user with private token when sudo defined' do it 'returns sudoed user with private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}") get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/login') expect(response).to match_response_schema('user/login')
expect(json_response['id']).to eq(user.id)
end end
it 'returns current user without private token when sudo not defined' do it 'returns initial current user without private token when sudo not defined' do
get api("/user?private_token=#{private_token}") get api("/user?private_token=#{admin.private_token}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(admin.id)
end end
end end
end end
......
...@@ -694,7 +694,7 @@ describe SystemNoteService, services: true do ...@@ -694,7 +694,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do describe "existing reference" do
before do before do
message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
end end
......
...@@ -75,7 +75,8 @@ module LoginHelpers ...@@ -75,7 +75,8 @@ module LoginHelpers
def logout def logout
find(".header-user-dropdown-toggle").click find(".header-user-dropdown-toggle").click
click_link "Sign out" click_link "Sign out"
expect(page).to have_content('Signed out successfully') # check the sign_in button
expect(page).to have_button('Sign in')
end end
# Logout without JavaScript driver # Logout without JavaScript driver
......
...@@ -16,8 +16,14 @@ Service.available_services_names.each do |service| ...@@ -16,8 +16,14 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken') hash.merge!(k => 'secrettoken')
elsif k =~ /^(.*_url|url|webhook)/ elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com") hash.merge!(k => "http://example.com")
elsif service_klass.method_defined?("#{k}?")
hash.merge!(k => true)
elsif service == 'irker' && k == :recipients elsif service == 'irker' && k == :recipients
hash.merge!(k => 'irc://irc.network.net:666/#channel') hash.merge!(k => 'irc://irc.network.net:666/#channel')
elsif service == 'irker' && k == :server_port
hash.merge!(k => 1234)
elsif service == 'jira' && k == :jira_issue_transition_id
hash.merge!(k => 1234)
else else
hash.merge!(k => "someword") hash.merge!(k => "someword")
end end
......
...@@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do ...@@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows the commit SHA' do it 'shows the commit SHA' do
render render
expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}") expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end end
it 'shows the last pipeline that ran for the commit' do it 'shows the last pipeline that ran for the commit' do
......
...@@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do ...@@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do
it 'shows a graph with grouped stages' do it 'shows a graph with grouped stages' do
render render
expect(rendered).to have_css('.pipeline-graph') expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.grouped-pipeline-dropdown') expect(rendered).to have_css('.grouped-pipeline-dropdown')
# stages # stages
......
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