Commit dd8d40c3 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-upstream' into 'master'

CE upstream

Closes gitaly#109 and gitlab-ce#28515

See merge request !1371
parents ee4d5504 24177b42
...@@ -423,3 +423,4 @@ cache gems: ...@@ -423,3 +423,4 @@ cache gems:
- vendor/cache - vendor/cache
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
...@@ -81,7 +81,7 @@ gem 'kaminari', '~> 0.17.0' ...@@ -81,7 +81,7 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
# Files attachments # Files attachments
gem 'carrierwave', '~> 0.10.0' gem 'carrierwave', '~> 0.11.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
...@@ -115,7 +115,7 @@ gem 'faraday_middleware-aws-signers-v4' ...@@ -115,7 +115,7 @@ gem 'faraday_middleware-aws-signers-v4'
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1' gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3' gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
...@@ -241,7 +241,7 @@ gem 'uglifier', '~> 2.7.2' ...@@ -241,7 +241,7 @@ gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.6.1' gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.0' gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0' gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
......
...@@ -111,11 +111,12 @@ GEM ...@@ -111,11 +111,12 @@ GEM
capybara-screenshot (1.0.11) capybara-screenshot (1.0.11)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (0.10.0) carrierwave (0.11.2)
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
json (>= 1.7) json (>= 1.7)
mime-types (>= 1.16) mime-types (>= 1.16)
mimemagic (>= 0.3.0)
cause (0.1) cause (0.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chronic (0.10.2) chronic (0.10.2)
...@@ -255,7 +256,7 @@ GEM ...@@ -255,7 +256,7 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.6.1.0) font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
...@@ -619,7 +620,7 @@ GEM ...@@ -619,7 +620,7 @@ GEM
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.3.3) redcarpet (3.4.0)
redis (3.2.2) redis (3.2.2)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
...@@ -887,7 +888,7 @@ DEPENDENCIES ...@@ -887,7 +888,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.6.2) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0) carrierwave (~> 0.11.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
...@@ -918,7 +919,7 @@ DEPENDENCIES ...@@ -918,7 +919,7 @@ DEPENDENCIES
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.6.1) font-awesome-rails (~> 4.7)
foreman (~> 0.78.0) foreman (~> 0.78.0)
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
...@@ -1000,7 +1001,7 @@ DEPENDENCIES ...@@ -1000,7 +1001,7 @@ DEPENDENCIES
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.3.3) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1) redis-rails (~> 5.0.1)
......
class BindInOut {
constructor(bindIn, bindOut) {
this.in = bindIn;
this.out = bindOut;
this.eventWrapper = {};
this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change';
}
addEvents() {
this.eventWrapper.updateOut = this.updateOut.bind(this);
this.in.addEventListener(this.eventType, this.eventWrapper.updateOut);
return this;
}
updateOut() {
this.out.textContent = this.in.value;
return this;
}
removeEvents() {
this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut);
return this;
}
static initAll() {
const ins = document.querySelectorAll('*[data-bind-in]');
return [].map.call(ins, anIn => BindInOut.init(anIn));
}
static init(anIn, anOut) {
const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`);
if (!out) return null;
const bindInOut = new BindInOut(anIn, out);
return bindInOut.addEvents().updateOut();
}
}
export default BindInOut;
...@@ -21,8 +21,7 @@ ...@@ -21,8 +21,7 @@
// %a.js-toggle-button // %a.js-toggle-button
// %div.js-toggle-content // %div.js-toggle-content
// //
$('body').on('click', '.js-toggle-button', function(e) { $('body').on('click', '.js-toggle-button', function() {
e.preventDefault();
toggleContainer($(this).closest('.js-toggle-container')); toggleContainer($(this).closest('.js-toggle-container'));
}); });
......
...@@ -113,7 +113,7 @@ require('./lib/utils/common_utils'); ...@@ -113,7 +113,7 @@ require('./lib/utils/common_utils');
return `<dl>\n${lines.join('\n')}\n</dl>`; return `<dl>\n${lines.join('\n')}\n</dl>`;
}, },
'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
const tag = el.nodeName.toLowerCase(); const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`; return `<${tag}>${text}</${tag}>`;
}, },
......
...@@ -38,6 +38,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make ...@@ -38,6 +38,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
...@@ -233,9 +234,14 @@ const UserCallout = require('./user_callout'); ...@@ -233,9 +234,14 @@ const UserCallout = require('./user_callout');
new UsersSelect(); new UsersSelect();
break; break;
case 'groups:new': case 'groups:new':
case 'admin:groups:new':
case 'groups:create':
case 'admin:groups:create':
BindInOut.initAll();
case 'groups:new':
case 'admin:groups:new':
case 'groups:edit': case 'groups:edit':
case 'admin:groups:edit': case 'admin:groups:edit':
case 'admin:groups:new':
new GroupAvatar(); new GroupAvatar();
break; break;
case 'projects:tree:show': case 'projects:tree:show':
......
/* eslint-disable func-names, space-before-function-paren */
/*= require raphael */
/*= require g.raphael */
/*= require g.bar */
(function() {
}).call(window);
...@@ -49,6 +49,7 @@ require('./behaviors/details_behavior'); ...@@ -49,6 +49,7 @@ require('./behaviors/details_behavior');
require('./behaviors/quick_submit'); require('./behaviors/quick_submit');
require('./behaviors/requires_input'); require('./behaviors/requires_input');
require('./behaviors/toggler_behavior'); require('./behaviors/toggler_behavior');
require('./behaviors/bind_in_out');
// blob // blob
require('./blob/blob_ci_yaml'); require('./blob/blob_ci_yaml');
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
/* global BranchGraph */
(function() { import BranchGraph from './branch_graph';
this.Network = (function() {
function Network(opts) {
var vph;
$("#filter_ref").click(function() {
return $(this).closest('form').submit();
});
this.branch_graph = new BranchGraph($(".network-graph"), opts);
vph = $(window).height() - 250;
$('.network-graph').css({
'height': vph + 'px'
});
}
return Network; export default (function() {
})(); function Network(opts) {
}).call(window); var vph;
$("#filter_ref").click(function() {
return $(this).closest('form').submit();
});
this.branch_graph = new BranchGraph($(".network-graph"), opts);
vph = $(window).height() - 250;
$('.network-graph').css({
'height': vph + 'px'
});
}
return Network;
})();
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
/* global Network */
/* global ShortcutsNetwork */ /* global ShortcutsNetwork */
require('./branch_graph'); import Network from './network';
require('./network');
(function() { $(function() {
$(function() { if (!$(".network-graph").length) return;
if (!$(".network-graph").length) return;
var network_graph; var network_graph;
network_graph = new Network({ network_graph = new Network({
url: $(".network-graph").attr('data-url'), url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'), commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'), ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id') commit_id: $(".network-graph").attr('data-commit-id')
});
return new ShortcutsNetwork(network_graph.branch_graph);
}); });
}).call(window); return new ShortcutsNetwork(network_graph.branch_graph);
});
import Raphael from 'raphael/raphael';
Raphael.prototype.commitTooltip = function commitTooltip(x, y, commit) {
const boxWidth = 300;
const icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
const nameText = this.text(x + 25, y + 10, commit.author.name);
const idText = this.text(x, y + 35, commit.id);
const messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, ' \n '));
const textSet = this.set(icon, nameText, idText, messageText).attr({
'text-anchor': 'start',
font: '12px Monaco, monospace',
});
nameText.attr({
font: '14px Arial',
'font-weight': 'bold',
});
idText.attr({
fill: '#AAA',
});
messageText.node.style['white-space'] = 'pre';
this.textWrap(messageText, boxWidth - 50);
const rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
fill: '#FFF',
stroke: '#000',
'stroke-linecap': 'round',
'stroke-width': 2,
});
const tooltip = this.set(rect, textSet);
rect.attr({
height: tooltip.getBBox().height + 10,
width: tooltip.getBBox().width + 10,
});
tooltip.transform(['t', 20, 20]);
return tooltip;
};
Raphael.prototype.textWrap = function testWrap(t, width) {
const content = t.attr('text');
const abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
t.attr({
text: abc,
});
const letterWidth = t.getBBox().width / abc.length;
t.attr({
text: content,
});
const words = content.split(' ');
let x = 0;
const s = [];
for (let j = 0, len = words.length; j < len; j += 1) {
const word = words[j];
if (x + (word.length * letterWidth) > width) {
s.push('\n');
x = 0;
}
if (word === '\n') {
s.push('\n');
x = 0;
} else {
s.push(`${word} `);
x += word.length * letterWidth;
}
}
t.attr({
text: s.join('').trim(),
});
const b = t.getBBox();
const h = Math.abs(b.y2) + 1;
return t.attr({
y: h,
});
};
export default Raphael;
...@@ -198,7 +198,7 @@ require('./task_list'); ...@@ -198,7 +198,7 @@ require('./task_list');
this.refreshing = true; this.refreshing = true;
return $.ajax({ return $.ajax({
url: this.notes_url, url: this.notes_url,
data: "last_fetched_at=" + this.last_fetched_at, headers: { "X-Last-Fetched-At": this.last_fetched_at },
dataType: "json", dataType: "json",
success: (function(_this) { success: (function(_this) {
return function(data) { return function(data) {
......
...@@ -16,6 +16,9 @@ require('./shortcuts'); ...@@ -16,6 +16,9 @@ require('./shortcuts');
Mousetrap.bind('g p', function() { Mousetrap.bind('g p', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); return ShortcutsNavigation.findAndFollowLink('.shortcuts-project');
}); });
Mousetrap.bind('g e', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity');
});
Mousetrap.bind('g f', function() { Mousetrap.bind('g f', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree');
}); });
...@@ -28,6 +31,9 @@ require('./shortcuts'); ...@@ -28,6 +31,9 @@ require('./shortcuts');
Mousetrap.bind('g n', function() { Mousetrap.bind('g n', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
}); });
Mousetrap.bind('g g', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts');
});
Mousetrap.bind('g i', function() { Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
}); });
......
...@@ -60,6 +60,15 @@ ...@@ -60,6 +60,15 @@
}); });
}; };
$('.assign-to-me-link').on('click', (e) => {
e.preventDefault();
$(e.currentTarget).hide();
const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
$input.val(gon.current_user_id);
selectedId = $input.val();
$dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
});
$block.on('click', '.js-assign-yourself', function(e) { $block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault(); e.preventDefault();
...@@ -199,6 +208,11 @@ ...@@ -199,6 +208,11 @@
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault(); e.preventDefault();
selectedId = user.id; selectedId = user.id;
if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide();
} else {
$('.assign-to-me-link').show();
}
return; return;
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
...@@ -234,11 +248,16 @@ ...@@ -234,11 +248,16 @@
id: function (user) { id: function (user) {
return user.id; return user.id;
}, },
opened: function(e) {
const $el = $(e.currentTarget);
$el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
},
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";
avatar = user.avatar_url ? user.avatar_url : false; avatar = user.avatar_url ? user.avatar_url : false;
selected = user.id === selectedId ? "is-active" : ""; selected = user.id === parseInt(selectedId, 10) ? "is-active" : "";
img = ""; img = "";
if (user.beforeDivider != null) { if (user.beforeDivider != null) {
"<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>"; "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>";
...@@ -248,7 +267,7 @@ ...@@ -248,7 +267,7 @@
} }
} }
// split into three parts so we can remove the username section if nessesary // split into three parts so we can remove the username section if nessesary
listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
listClosingTags = "</a> </li>"; listClosingTags = "</a> </li>";
if (username === '') { if (username === '') {
......
...@@ -96,7 +96,7 @@ ...@@ -96,7 +96,7 @@
.dropdown-menu-toggle { .dropdown-menu-toggle {
@extend .dropdown-toggle; @extend .dropdown-toggle;
padding-right: 20px; padding-right: 25px;
position: relative; position: relative;
width: 163px; width: 163px;
text-overflow: ellipsis; text-overflow: ellipsis;
......
.filter-item { .filter-item {
margin-right: 6px;
vertical-align: top; vertical-align: top;
&.reset-filters { &.reset-filters {
...@@ -14,6 +13,20 @@ ...@@ -14,6 +13,20 @@
width: 132px; width: 132px;
} }
} }
.filter-item:not(:last-child) {
margin-right: 6px;
}
.sort-filter {
display: inline-block;
float: right;
}
.dropdown-menu-sort {
left: auto;
right: 0;
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
...@@ -260,24 +260,34 @@ header { ...@@ -260,24 +260,34 @@ header {
font-size: 18px; font-size: 18px;
.navbar-nav { .navbar-nav {
display: table;
table-layout: fixed;
width: 100%;
margin: 0; margin: 0;
float: none !important; text-align: right;
.visible-xs,
.visible-sm {
display: table-cell !important;
}
} }
.navbar-collapse { .navbar-collapse {
padding-left: 5px; padding-left: 5px;
.nav > li { .nav > li:not(.hidden-xs) {
display: table-cell; display: table-cell!important;
width: 1%; width: 25%;
a {
margin-right: 8px;
}
} }
} }
} }
.header-user-dropdown-toggle {
text-align: center;
}
.header-user-avatar {
float: none;
}
} }
.header-user { .header-user {
......
...@@ -8,6 +8,19 @@ body { ...@@ -8,6 +8,19 @@ body {
&.navless { &.navless {
background-color: $white-light !important; background-color: $white-light !important;
} }
&.card-content {
background-color: $gray-darker;
.content-wrapper {
padding: 0;
.container-fluid,
.container-limited {
background-color: $gray-darker;
}
}
}
} }
.container { .container {
......
...@@ -86,6 +86,16 @@ ...@@ -86,6 +86,16 @@
position: fixed; position: fixed;
} }
/*
* Fix <summary> elements on firefox
* See https://github.com/necolas/normalize.css/issues/640
* and https://github.com/twbs/bootstrap/issues/21060
*
*/
summary {
display: list-item;
}
@import "bootstrap/responsive-utilities"; @import "bootstrap/responsive-utilities";
// Labels // Labels
......
...@@ -20,8 +20,9 @@ ...@@ -20,8 +20,9 @@
outline: none; outline: none;
resize: none; resize: none;
height: 100vh; height: 100vh;
max-height: calc(100vh - 10px);
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto 10px;
} }
.zen-control-leave { .zen-control-leave {
......
...@@ -121,3 +121,19 @@ ...@@ -121,3 +121,19 @@
table.pipeline-project-metrics tr td { table.pipeline-project-metrics tr td {
padding: $gl-padding; padding: $gl-padding;
} }
.mattermost-icon svg {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
.mattermost-team-name {
color: $gl-text-color-secondary;
}
.mattermost-info {
display: block;
color: $gl-text-color-secondary;
margin-top: 10px;
}
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
* *
*/ */
.mr-state-widget { .mr-state-widget {
background: $gray-light;
color: $gl-text-color; color: $gl-text-color;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 2px; border-radius: 2px;
...@@ -109,12 +108,17 @@ ...@@ -109,12 +108,17 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
flex-wrap: wrap; flex-wrap: wrap;
} }
.ci-status-icon > .icon-link > svg {
width: 22px;
height: 22px;
}
} }
.mr-widget-body, .mr-widget-body,
.ci_widget, .ci_widget,
.mr-widget-footer { .mr-widget-footer {
padding: $gl-padding; padding: 16px;
} }
.mr-widget-pipeline-graph { .mr-widget-pipeline-graph {
...@@ -174,10 +178,6 @@ ...@@ -174,10 +178,6 @@
} }
} }
p:last-child {
margin-bottom: 0;
}
.btn-grouped { .btn-grouped {
margin-left: 0; margin-left: 0;
margin-right: 7px; margin-right: 7px;
...@@ -340,8 +340,61 @@ ...@@ -340,8 +340,61 @@
} }
} }
.remove-message-pipes {
ul {
margin: 10px 0 0 12px;
padding: 0;
list-style: none;
border-left: 2px solid $border-color;
display: inline-block;
}
li {
position: relative;
margin: 0;
padding: 0;
display: block;
span {
margin-left: 15px;
max-height: 20px;
}
}
li::before {
content: '';
position: absolute;
border-top: 2px solid $border-color;
height: 1px;
top: 8px;
width: 8px;
}
li:last-child {
&::before {
top: 18px;
}
span {
display: block;
position: relative;
top: 5px;
margin-top: 5px;
}
}
}
.mr-source-target { .mr-source-target {
background-color: $gray-light;
line-height: 31px; line-height: 31px;
border-style: solid;
border-width: 1px;
border-color: $border-color;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
border-bottom: none;
padding: 16px;
margin-bottom: -1px;
} }
.panel-new-merge-request { .panel-new-merge-request {
...@@ -426,6 +479,11 @@ ...@@ -426,6 +479,11 @@
} }
} }
.assign-to-me-link {
padding-left: 12px;
white-space: nowrap;
}
.table-holder { .table-holder {
.ci-table { .ci-table {
...@@ -437,6 +495,8 @@ ...@@ -437,6 +495,8 @@
} }
.merged-buttons { .merged-buttons {
margin-top: 20px;
.btn { .btn {
float: left; float: left;
......
...@@ -494,11 +494,11 @@ a.deploy-project-label { ...@@ -494,11 +494,11 @@ a.deploy-project-label {
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid $border-color;
} }
.nav > li { .nav > li {
...@@ -645,30 +645,15 @@ pre.light-well { ...@@ -645,30 +645,15 @@ pre.light-well {
} }
.project-last-commit { .project-last-commit {
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-base;
padding: 12px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-top: $gl-padding; margin-top: $gl-padding;
} }
&.container-fluid {
padding-top: 12px;
padding-bottom: 12px;
background-color: $gray-light;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
@media (min-width: $screen-sm-min) {
border-right-width: 1px;
border-left-width: 1px;
}
}
&.container-limited {
@media (min-width: 1281px) {
border-radius: $border-radius-base;
}
}
.ci-status { .ci-status {
margin-right: $gl-padding; margin-right: $gl-padding;
} }
......
...@@ -170,7 +170,11 @@ ...@@ -170,7 +170,11 @@
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.todos-filters { .todos-filters {
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 135px; width: 130px;
}
.dropdown-menu-toggle-sort {
width: auto;
} }
} }
} }
...@@ -200,10 +204,6 @@ ...@@ -200,10 +204,6 @@
} }
.todos-filters { .todos-filters {
.row-content-block {
padding-bottom: 50px;
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
} }
......
...@@ -147,6 +147,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -147,6 +147,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:two_factor_grace_period, :two_factor_grace_period,
:user_default_external, :user_default_external,
:user_oauth_applications, :user_oauth_applications,
:unique_ips_limit_per_user,
:unique_ips_limit_time_window,
:unique_ips_limit_enabled,
:version_check_enabled, :version_check_enabled,
:terminal_max_session_time, :terminal_max_session_time,
......
...@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base ...@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
render_403 render_403
end end
rescue_from Gitlab::Auth::TooManyIps do |e|
head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window
end
def redirect_back_or_default(default: root_path, options: {}) def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options redirect_to request.referer.present? ? :back : default, options
end end
......
...@@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController ...@@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController
@group = Groups::CreateService.new(current_user, group_params).execute @group = Groups::CreateService.new(current_user, group_params).execute
if @group.persisted? if @group.persisted?
redirect_to @group, notice: "Group '#{@group.name}' was successfully created." notice = if @group.chat_team.present?
"Group '#{@group.name}' and its Mattermost team were successfully created."
else
"Group '#{@group.name}' was successfully created."
end
redirect_to @group, notice: notice
else else
render action: "new" render action: "new"
end end
...@@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController ...@@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled, :request_access_enabled,
:share_with_group_lock, :share_with_group_lock,
:visibility_level, :visibility_level,
:parent_id :parent_id,
:create_chat_team,
:chat_team_name
] ]
end end
......
...@@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController ...@@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController
end end
def update_username def update_username
@user.update_attributes(username: user_params[:username]) if @user.update_attributes(username: user_params[:username])
options = { notice: "Username successfully changed" }
respond_to do |format| else
format.js message = @user.errors.full_messages.uniq.join('. ')
options = { alert: "Username change failed - #{message}" }
end end
redirect_back_or_default(default: { action: 'show' }, options: options)
end end
private private
......
...@@ -20,7 +20,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -20,7 +20,7 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: @repository.branch_names render json: @branches.map(&:name)
end end
end end
end end
......
...@@ -211,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -211,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController
end end
def find_current_user_notes def find_current_user_notes
@notes = NotesFinder.new(project, current_user, params).execute.inc_author @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
.execute.inc_author
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end end
end end
...@@ -34,16 +34,18 @@ class IssuableFinder ...@@ -34,16 +34,18 @@ class IssuableFinder
items = by_scope(items) items = by_scope(items)
items = by_state(items) items = by_state(items)
items = by_group(items) items = by_group(items)
items = by_project(items)
items = by_search(items) items = by_search(items)
items = by_milestone(items)
items = by_assignee(items) items = by_assignee(items)
items = by_author(items) items = by_author(items)
items = by_label(items)
items = by_weight(items) items = by_weight(items)
items = by_due_date(items) items = by_due_date(items)
items = by_non_archived(items) items = by_non_archived(items)
items = by_iids(items) items = by_iids(items)
items = by_milestone(items)
items = by_label(items)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items = by_project(items)
sort(items) sort(items)
end end
...@@ -109,8 +111,7 @@ class IssuableFinder ...@@ -109,8 +111,7 @@ class IssuableFinder
@project = project @project = project
end end
def projects def projects(items = nil)
return @projects if defined?(@projects)
return @projects = project if project? return @projects = project if project?
projects = projects =
...@@ -119,7 +120,7 @@ class IssuableFinder ...@@ -119,7 +120,7 @@ class IssuableFinder
elsif group elsif group
GroupProjectsFinder.new(group).execute(current_user) GroupProjectsFinder.new(group).execute(current_user)
else else
ProjectsFinder.new.execute(current_user) projects_finder.execute(current_user, item_project_ids(items))
end end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
...@@ -259,9 +260,9 @@ class IssuableFinder ...@@ -259,9 +260,9 @@ class IssuableFinder
def by_project(items) def by_project(items)
items = items =
if project? if project?
items.of_projects(projects).references_project items.of_projects(projects(items)).references_project
elsif projects elsif projects(items)
items.merge(projects.reorder(nil)).join_project items.merge(projects(items).reorder(nil)).join_project
else else
items.none items.none
end end
...@@ -316,13 +317,14 @@ class IssuableFinder ...@@ -316,13 +317,14 @@ class IssuableFinder
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil]) items = items.left_joins_milestones.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone? elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects) upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids) items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
else else
items = items.with_milestone(params[:milestone_title]) items = items.with_milestone(params[:milestone_title])
items_projects = projects(items)
if projects if items_projects
items = items.where(milestones: { project_id: projects }) items = items.where(milestones: { project_id: items_projects })
end end
end end
end end
...@@ -336,9 +338,10 @@ class IssuableFinder ...@@ -336,9 +338,10 @@ class IssuableFinder
items = items.without_label items = items.without_label
else else
items = items.with_label(label_names, params[:sort]) items = items.with_label(label_names, params[:sort])
items_projects = projects(items)
if projects if items_projects
label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids }) items = items.where(labels: { id: label_ids })
end end
end end
...@@ -423,4 +426,8 @@ class IssuableFinder ...@@ -423,4 +426,8 @@ class IssuableFinder
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end end
...@@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder ...@@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder
user_id: user.id, user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end end
def item_project_ids(items)
items&.reorder(nil)&.select(:project_id)
end
end end
...@@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder ...@@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder
def klass def klass
MergeRequest MergeRequest
end end
private
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
end end
...@@ -12,29 +12,43 @@ module LicenseHelper ...@@ -12,29 +12,43 @@ module LicenseHelper
HistoricalData.max_historical_user_count HistoricalData.max_historical_user_count
end end
def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?)) # in_html is set to false from an initializer, which shouldn't try to render
@license_message ||= # HTML links.
#
def license_message(signed_in: signed_in?, is_admin: (current_user && current_user.is_admin?), in_html: true)
@license_message =
if License.current if License.current
yes_license_message(signed_in, is_admin) yes_license_message(signed_in, is_admin)
else else
no_license_message(is_admin) no_license_message(is_admin, in_html: in_html)
end end
end end
private private
def no_license_message(is_admin) def no_license_message(is_admin, in_html: true)
upload_a_license =
if in_html
link_to('Upload a license', new_admin_license_path)
else
'Upload a license'
end
message = [] message = []
message << 'No GitLab Enterprise Edition license has been provided yet.' message << 'No GitLab Enterprise Edition license has been provided yet.'
message << 'Pushing code and creation of issues and merge requests has been disabled.' message << 'Pushing code and creation of issues and merge requests has been disabled.'
message << message <<
if is_admin if is_admin
"#{link_to('Upload a license', new_admin_license_path)} in the admin area to activate this functionality." "#{upload_a_license} in the admin area to activate this functionality."
else else
'Ask an admin to upload a license to activate this functionality.' 'Ask an admin to upload a license to activate this functionality.'
end end
content_tag(:p, message.join(' ').html_safe) if in_html
content_tag(:p, message.join(' ').html_safe)
else
message.join(' ')
end
end end
def yes_license_message(signed_in, is_admin) def yes_license_message(signed_in, is_admin)
......
module MattermostHelper module MattermostHelper
def mattermost_teams_options(teams) def mattermost_teams_options(teams)
teams_options = teams.map do |id, options| teams.map do |team|
[options['display_name'] || options['name'], id] [team['display_name'] || team['name'], team['id']]
end end
teams_options.compact.unshift(['Select team...', '0'])
end end
end end
module TriggersHelper module TriggersHelper
def builds_trigger_url(project_id, ref: nil) def builds_trigger_url(project_id, ref: nil)
if ref.nil? if ref.nil?
"#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline"
else else
"#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline"
end end
end end
......
...@@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base ...@@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy
end end
...@@ -65,6 +65,16 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -65,6 +65,16 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
if: :akismet_enabled if: :akismet_enabled
validates :unique_ips_limit_per_user,
numericality: { greater_than_or_equal_to: 1 },
presence: true,
if: :unique_ips_limit_enabled
validates :unique_ips_limit_time_window,
numericality: { greater_than_or_equal_to: 0 },
presence: true,
if: :unique_ips_limit_enabled
validates :koding_url, validates :koding_url,
presence: true, presence: true,
if: :koding_enabled if: :koding_enabled
...@@ -203,6 +213,9 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -203,6 +213,9 @@ class ApplicationSetting < ActiveRecord::Base
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil, help_page_text: nil,
unique_ips_limit_per_user: 10,
unique_ips_limit_time_window: 3600,
unique_ips_limit_enabled: false,
housekeeping_bitmaps_enabled: true, housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true, housekeeping_enabled: true,
housekeeping_full_repack_period: 50, housekeeping_full_repack_period: 50,
......
class ChatTeam < ActiveRecord::Base
validates :team_id, presence: true
belongs_to :namespace
end
...@@ -64,6 +64,10 @@ module Ci ...@@ -64,6 +64,10 @@ module Ci
end end
state_machine :status do state_machine :status do
event :actionize do
transition created: :manual
end
after_transition any => [:pending] do |build| after_transition any => [:pending] do |build|
build.run_after_commit do build.run_after_commit do
BuildQueueWorker.perform_async(id) BuildQueueWorker.perform_async(id)
...@@ -95,16 +99,21 @@ module Ci ...@@ -95,16 +99,21 @@ module Ci
.fabricate! .fabricate!
end end
def manual?
self.when == 'manual'
end
def other_actions def other_actions
pipeline.manual_actions.where.not(name: name) pipeline.manual_actions.where.not(name: name)
end end
def playable? def playable?
project.builds_enabled? && commands.present? && manual? && skipped? project.builds_enabled? && has_commands? &&
action? && manual?
end
def action?
self.when == 'manual'
end
def has_commands?
commands.present?
end end
def play(current_user) def play(current_user)
...@@ -123,7 +132,7 @@ module Ci ...@@ -123,7 +132,7 @@ module Ci
end end
def retryable? def retryable?
project.builds_enabled? && commands.present? && project.builds_enabled? && has_commands? &&
(success? || failed? || canceled?) (success? || failed? || canceled?)
end end
...@@ -553,7 +562,7 @@ module Ci ...@@ -553,7 +562,7 @@ module Ci
] ]
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag? variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual? variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action?
variables variables
end end
......
...@@ -49,6 +49,10 @@ module Ci ...@@ -49,6 +49,10 @@ module Ci
transition any - [:canceled] => :canceled transition any - [:canceled] => :canceled
end end
event :block do
transition any - [:manual] => :manual
end
# IMPORTANT # IMPORTANT
# Do not add any operations to this state_machine # Do not add any operations to this state_machine
# Create a separate worker for each new operation # Create a separate worker for each new operation
...@@ -321,6 +325,7 @@ module Ci ...@@ -321,6 +325,7 @@ module Ci
when 'failed' then drop when 'failed' then drop
when 'canceled' then cancel when 'canceled' then cancel
when 'skipped' then skip when 'skipped' then skip
when 'manual' then block
end end
end end
end end
......
...@@ -127,18 +127,15 @@ module Ci ...@@ -127,18 +127,15 @@ module Ci
def tick_runner_queue def tick_runner_queue
SecureRandom.hex.tap do |new_update| SecureRandom.hex.tap do |new_update|
Gitlab::Redis.with do |redis| ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true)
end
end end
end end
def ensure_runner_queue_value def ensure_runner_queue_value
Gitlab::Redis.with do |redis| new_value = SecureRandom.hex
value = SecureRandom.hex ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value,
redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true) expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
redis.get(runner_queue_key)
end
end end
def is_runner_queue_value_latest?(value) def is_runner_queue_value_latest?(value)
......
...@@ -5,10 +5,11 @@ module Ci ...@@ -5,10 +5,11 @@ module Ci
acts_as_paranoid acts_as_paranoid
belongs_to :project, foreign_key: :gl_project_id belongs_to :project, foreign_key: :gl_project_id
belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy has_many :trigger_requests, dependent: :destroy
validates :token, presence: true validates :token, presence: true, uniqueness: true
validates :token, uniqueness: true
before_validation :set_default_values before_validation :set_default_values
...@@ -25,7 +26,11 @@ module Ci ...@@ -25,7 +26,11 @@ module Ci
end end
def short_token def short_token
token[0...10] token[0...4]
end
def can_show_token?(user)
owner.blank? || owner == user
end end
end end
end end
...@@ -29,9 +29,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -29,9 +29,11 @@ class CommitStatus < ActiveRecord::Base
end end
scope :exclude_ignored, -> do scope :exclude_ignored, -> do
# We want to ignore failed_but_allowed jobs # We want to ignore failed but allowed to fail jobs.
#
# TODO, we also skip ignored optional manual actions.
where("allow_failure = ? OR status IN (?)", where("allow_failure = ? OR status IN (?)",
false, all_state_names - [:failed, :canceled]) false, all_state_names - [:failed, :canceled, :manual])
end end
scope :retried, -> { where.not(id: latest) } scope :retried, -> { where.not(id: latest) }
...@@ -42,11 +44,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -42,11 +44,11 @@ class CommitStatus < ActiveRecord::Base
state_machine :status do state_machine :status do
event :enqueue do event :enqueue do
transition [:created, :skipped] => :pending transition [:created, :skipped, :manual] => :pending
end end
event :process do event :process do
transition skipped: :created transition [:skipped, :manual] => :created
end end
event :run do event :run do
...@@ -66,7 +68,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -66,7 +68,7 @@ class CommitStatus < ActiveRecord::Base
end end
event :cancel do event :cancel do
transition [:created, :pending, :running] => :canceled transition [:created, :pending, :running, :manual] => :canceled
end end
before_transition created: [:pending, :running] do |commit_status| before_transition created: [:pending, :running] do |commit_status|
...@@ -86,7 +88,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -86,7 +88,7 @@ class CommitStatus < ActiveRecord::Base
commit_status.run_after_commit do commit_status.run_after_commit do
pipeline.try do |pipeline| pipeline.try do |pipeline|
if complete? if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id) PipelineProcessWorker.perform_async(pipeline.id)
else else
PipelineUpdateWorker.perform_async(pipeline.id) PipelineUpdateWorker.perform_async(pipeline.id)
......
...@@ -2,22 +2,21 @@ module HasStatus ...@@ -2,22 +2,21 @@ module HasStatus
extend ActiveSupport::Concern extend ActiveSupport::Concern
DEFAULT_STATUS = 'created'.freeze DEFAULT_STATUS = 'created'.freeze
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped].freeze BLOCKED_STATUS = 'manual'.freeze
STARTED_STATUSES = %w[running success failed skipped].freeze AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze
STARTED_STATUSES = %w[running success failed skipped manual].freeze
ACTIVE_STATUSES = %w[pending running].freeze ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed pending running canceled success skipped].freeze ORDERED_STATUSES = %w[manual failed pending running canceled success skipped].freeze
class_methods do class_methods do
def status_sql def status_sql
scope = if respond_to?(:exclude_ignored) scope = respond_to?(:exclude_ignored) ? exclude_ignored : all
exclude_ignored
else
all
end
builds = scope.select('count(*)').to_sql builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql
manual = scope.manual.select('count(*)').to_sql
pending = scope.pending.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql
...@@ -30,7 +29,8 @@ module HasStatus ...@@ -30,7 +29,8 @@ module HasStatus
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
ELSE 'failed' ELSE 'failed'
END)" END)"
end end
...@@ -63,6 +63,7 @@ module HasStatus ...@@ -63,6 +63,7 @@ module HasStatus
state :success, value: 'success' state :success, value: 'success'
state :canceled, value: 'canceled' state :canceled, value: 'canceled'
state :skipped, value: 'skipped' state :skipped, value: 'skipped'
state :manual, value: 'manual'
end end
scope :created, -> { where(status: 'created') } scope :created, -> { where(status: 'created') }
...@@ -73,12 +74,13 @@ module HasStatus ...@@ -73,12 +74,13 @@ module HasStatus
scope :failed, -> { where(status: 'failed') } scope :failed, -> { where(status: 'failed') }
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
scope :cancelable, -> do scope :cancelable, -> do
where(status: [:running, :pending, :created]) where(status: [:running, :pending, :created, :manual])
end end
end end
...@@ -94,6 +96,10 @@ module HasStatus ...@@ -94,6 +96,10 @@ module HasStatus
COMPLETED_STATUSES.include?(status) COMPLETED_STATUSES.include?(status)
end end
def blocked?
BLOCKED_STATUS == status
end
private private
def calculate_duration def calculate_duration
......
...@@ -24,6 +24,11 @@ class ExternalIssue ...@@ -24,6 +24,11 @@ class ExternalIssue
def ==(other) def ==(other)
other.is_a?(self.class) && (to_s == other.to_s) other.is_a?(self.class) && (to_s == other.to_s)
end end
alias_method :eql?, :==
def hash
[self.class, to_s].hash
end
def project def project
@project @project
......
...@@ -40,6 +40,7 @@ class Group < Namespace ...@@ -40,6 +40,7 @@ class Group < Namespace
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
mount_uploader :avatar, AvatarUploader mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy
after_create :post_create_hook after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
...@@ -256,4 +257,14 @@ class Group < Namespace ...@@ -256,4 +257,14 @@ class Group < Namespace
def users_with_parents def users_with_parents
User.where(id: members_with_parents.select(:user_id)) User.where(id: members_with_parents.select(:user_id))
end end
def mattermost_team_params
max_length = 59
{
name: path[0..max_length],
display_name: name[0..max_length],
type: public? ? 'O' : 'I' # Open vs Invite-only
}
end
end end
...@@ -21,6 +21,7 @@ class Namespace < ActiveRecord::Base ...@@ -21,6 +21,7 @@ class Namespace < ActiveRecord::Base
belongs_to :parent, class_name: "Namespace" belongs_to :parent, class_name: "Namespace"
has_many :children, class_name: "Namespace", foreign_key: :parent_id has_many :children, class_name: "Namespace", foreign_key: :parent_id
has_one :chat_team, dependent: :destroy
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
......
...@@ -87,6 +87,7 @@ class Note < ActiveRecord::Base ...@@ -87,6 +87,7 @@ class Note < ActiveRecord::Base
before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id before_validation :set_discussion_id
after_save :keep_around_commit, unless: :for_personal_snippet? after_save :keep_around_commit, unless: :for_personal_snippet?
after_save :expire_etag_cache
class << self class << self
def model_name def model_name
...@@ -278,4 +279,16 @@ class Note < ActiveRecord::Base ...@@ -278,4 +279,16 @@ class Note < ActiveRecord::Base
self.class.build_discussion_id(noteable_type, noteable_id || commit_id) self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
end end
end end
def expire_etag_cache
return unless for_issue?
key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path(
noteable.project.namespace,
noteable.project,
target_type: noteable_type.underscore,
target_id: noteable.id
)
Gitlab::EtagCaching::Store.new.touch(key)
end
end end
...@@ -236,6 +236,7 @@ class Project < ActiveRecord::Base ...@@ -236,6 +236,7 @@ class Project < ActiveRecord::Base
before_validation :mark_remote_mirrors_for_removal before_validation :mark_remote_mirrors_for_removal
mount_uploader :avatar, AvatarUploader mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy
# Scopes # Scopes
default_scope { where(pending_delete: false) } default_scope { where(pending_delete: false) }
......
class Upload < ActiveRecord::Base
# Upper limit for foreground checksum processing
CHECKSUM_THRESHOLD = 100.megabytes
belongs_to :model, polymorphic: true
validates :size, presence: true
validates :path, presence: true
validates :model, presence: true
validates :uploader, presence: true
before_save :calculate_checksum, if: :foreground_checksum?
after_commit :schedule_checksum, unless: :foreground_checksum?
def self.remove_path(path)
where(path: path).destroy_all
end
def self.record(uploader)
remove_path(uploader.relative_path)
create(
size: uploader.file.size,
path: uploader.relative_path,
model: uploader.model,
uploader: uploader.class.to_s
)
end
def absolute_path
return path unless relative_path?
uploader_class.absolute_path(self)
end
def calculate_checksum
return unless exist?
self.checksum = Digest::SHA256.file(absolute_path).hexdigest
end
def exist?
File.exist?(absolute_path)
end
private
def foreground_checksum?
size <= CHECKSUM_THRESHOLD
end
def schedule_checksum
UploadChecksumWorker.perform_async(id)
end
def relative_path?
!path.start_with?('/')
end
def uploader_class
Object.const_get(uploader)
end
end
...@@ -106,6 +106,7 @@ class User < ActiveRecord::Base ...@@ -106,6 +106,7 @@ class User < ActiveRecord::Base
# Protected Branch Access # Protected Branch Access
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
...@@ -201,6 +202,7 @@ class User < ActiveRecord::Base ...@@ -201,6 +202,7 @@ class User < ActiveRecord::Base
end end
mount_uploader :avatar, AvatarUploader mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy
# Scopes # Scopes
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
......
...@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity ...@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity
path_to(:retry_namespace_project_build, build) path_to(:retry_namespace_project_build, build)
end end
expose :play_path, if: ->(build, _) { build.manual? } do |build| expose :play_path, if: ->(build, _) { build.playable? } do |build|
path_to(:play_namespace_project_build, build) path_to(:play_namespace_project_build, build)
end end
......
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
def execute(project, trigger, ref, variables = nil) def execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables) trigger_request = trigger.trigger_requests.create(variables: variables)
pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref). pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
execute(ignore_skip_ci: true, trigger_request: trigger_request) execute(ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted? if pipeline.persisted?
trigger_request trigger_request
......
...@@ -22,6 +22,8 @@ module Ci ...@@ -22,6 +22,8 @@ module Ci
def process_stage(index) def process_stage(index)
current_status = status_for_prior_stages(index) current_status = status_for_prior_stages(index)
return if HasStatus::BLOCKED_STATUS == current_status
if HasStatus::COMPLETED_STATUSES.include?(current_status) if HasStatus::COMPLETED_STATUSES.include?(current_status)
created_builds_in_stage(index).select do |build| created_builds_in_stage(index).select do |build|
Gitlab::OptimisticLocking.retry_lock(build) do |subject| Gitlab::OptimisticLocking.retry_lock(build) do |subject|
...@@ -33,7 +35,7 @@ module Ci ...@@ -33,7 +35,7 @@ module Ci
def process_build(build, current_status) def process_build(build, current_status)
if valid_statuses_for_when(build.when).include?(current_status) if valid_statuses_for_when(build.when).include?(current_status)
build.enqueue build.action? ? build.actionize : build.enqueue
true true
else else
build.skip build.skip
...@@ -49,6 +51,8 @@ module Ci ...@@ -49,6 +51,8 @@ module Ci
%w[failed] %w[failed]
when 'always' when 'always'
%w[success failed skipped] %w[success failed skipped]
when 'manual'
%w[success]
else else
[] []
end end
......
...@@ -2,6 +2,7 @@ module Groups ...@@ -2,6 +2,7 @@ module Groups
class CreateService < Groups::BaseService class CreateService < Groups::BaseService
def initialize(user, params = {}) def initialize(user, params = {})
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@chat_team = @params.delete(:create_chat_team)
end end
def execute def execute
...@@ -24,9 +25,23 @@ module Groups ...@@ -24,9 +25,23 @@ module Groups
end end
@group.name ||= @group.path.dup @group.name ||= @group.path.dup
if create_chat_team?
response = Mattermost::CreateTeamService.new(@group, current_user).execute
return @group if @group.errors.any?
@group.build_chat_team(name: response['name'], team_id: response['id'])
end
@group.save @group.save
@group.add_owner(current_user) @group.add_owner(current_user)
@group @group
end end
private
def create_chat_team?
Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
end
end end
end end
module Mattermost
class CreateTeamService < ::BaseService
def initialize(group, current_user)
@group, @current_user = group, current_user
end
def execute
# The user that creates the team will be Team Admin
Mattermost::Team.new(current_user).create(@group.mattermost_team_params)
rescue Mattermost::ClientError => e
@group.errors.add(:mattermost_team, e.message)
end
end
end
...@@ -34,6 +34,8 @@ module Projects ...@@ -34,6 +34,8 @@ module Projects
end end
rescue => e rescue => e
error(e.message) error(e.message)
ensure
build.erase_artifacts! unless build.has_expiring_artifacts?
end end
private private
......
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include RecordsUploads
include UploaderHelper include UploaderHelper
storage :file storage :file
......
class AvatarUploader < GitlabUploader class AvatarUploader < GitlabUploader
include RecordsUploads
include UploaderHelper include UploaderHelper
storage :file storage :file
......
class FileUploader < GitlabUploader class FileUploader < GitlabUploader
include RecordsUploads
include UploaderHelper include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file storage :file
def self.absolute_path(upload_record)
File.join(
self.dynamic_path_segment(upload_record.model),
upload_record.path
)
end
# Returns the part of `store_dir` that can change based on the model's current
# path
#
# This is used to build Upload paths dynamically based on the model's current
# namespace and path, allowing us to ignore renames or transfers.
#
# model - Object that responds to `path_with_namespace`
#
# Returns a String without a trailing slash
def self.dynamic_path_segment(model)
File.join(CarrierWave.root, base_dir, model.path_with_namespace)
end
attr_accessor :project attr_accessor :project
attr_reader :secret attr_reader :secret
...@@ -13,13 +35,21 @@ class FileUploader < GitlabUploader ...@@ -13,13 +35,21 @@ class FileUploader < GitlabUploader
end end
def store_dir def store_dir
File.join(base_dir, @project.path_with_namespace, @secret) File.join(dynamic_path_segment, @secret)
end end
def cache_dir def cache_dir
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end end
def model
project
end
def relative_path
self.file.path.sub("#{dynamic_path_segment}/", '')
end
def to_markdown def to_markdown
to_h[:markdown] to_h[:markdown]
end end
...@@ -40,6 +70,10 @@ class FileUploader < GitlabUploader ...@@ -40,6 +70,10 @@ class FileUploader < GitlabUploader
private private
def dynamic_path_segment
self.class.dynamic_path_segment(model)
end
def generate_secret def generate_secret
SecureRandom.hex SecureRandom.hex
end end
......
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
def self.absolute_path(upload_record)
File.join(CarrierWave.root, upload_record.path)
end
def self.base_dir def self.base_dir
'uploads' 'uploads'
end end
...@@ -18,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -18,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base
def move_to_store def move_to_store
true true
end end
# Designed to be overridden by child uploaders that have a dynamic path
# segment -- that is, a path that changes based on mutable attributes of its
# associated model
#
# For example, `FileUploader` builds the storage path based on the associated
# project model's `path_with_namespace` value, which can change when the
# project or its containing namespace is moved or renamed.
def relative_path
self.file.path.sub("#{root}/", '')
end
end end
module RecordsUploads
extend ActiveSupport::Concern
included do
after :store, :record_upload
before :remove, :destroy_upload
end
private
# After storing an attachment, create a corresponding Upload record
#
# NOTE: We're ignoring the argument passed to this callback because we want
# the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
# `Tempfile` object the callback gets.
#
# Called `after :store`
def record_upload(_tempfile)
return unless file_storage?
return unless file.exists?
Upload.record(self)
end
# Before removing an attachment, destroy any Upload records at the same path
#
# Called `before :remove`
def destroy_upload(*args)
return unless file_storage?
return unless file
Upload.remove_path(relative_path)
end
end
...@@ -380,6 +380,29 @@ ...@@ -380,6 +380,29 @@
Generate API key at Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :unique_ips_limit_enabled do
= f.check_box :unique_ips_limit_enabled
Limit sign in from multiple ips
%span.help-block#unique_ip_help_block
Helps prevent malicious users hide their activity
.form-group
= f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_per_user, class: 'form-control'
.help-block
Maximum number of unique IPs per user
.form-group
= f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_time_window, class: 'form-control'
.help-block
How many seconds an IP will be counted towards the limit
%fieldset %fieldset
%legend Abuse reports %legend Abuse reports
.form-group .form-group
......
...@@ -46,16 +46,16 @@ ...@@ -46,16 +46,16 @@
= hidden_field_tag(:action_id, params[:action_id]) = hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
data: { data: todo_actions_options, default_label: 'Action' } }) data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right .filter-item.sort-filter
.dropdown.inline.prepend-left-10 .dropdown
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light %span.light
- if @sort.present? - if @sort.present?
= sort_options_hash[@sort] = sort_options_hash[@sort]
- else - else
= sort_title_recently_created = sort_title_recently_created
= icon('chevron-down') = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %ul.dropdown-menu.dropdown-menu-sort
%li %li
= link_to todos_filter_path(sort: sort_value_priority) do = link_to todos_filter_path(sort: sort_value_priority) do
= sort_title_priority = sort_title_priority
......
.form-group
= f.label :create_chat_team, class: 'control-label' do
%span.mattermost-icon
= custom_icon('icon_mattermost')
Mattermost
.col-sm-10
.checkbox.js-toggle-container
= f.label :create_chat_team do
.js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false)
Create a Mattermost team for this group
%br
%small.light.js-toggle-content
Mattermost URL:
= Settings.mattermost.host
%span> /
%span{ "data-bind-out" => "create_chat_team" }
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
= render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group
= render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
= render 'shared/group_tips' = render 'shared/group_tips'
......
...@@ -128,6 +128,12 @@ ...@@ -128,6 +128,12 @@
.key p .key p
%td %td
Go to the project's home page Go to the project's home page
%tr
%td.shortcut
.key g
.key e
%td
Go to the project's activity feed
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
...@@ -152,6 +158,12 @@ ...@@ -152,6 +158,12 @@
.key n .key n
%td %td
Go to network graph Go to network graph
%tr
%td.shortcut
.key g
.key g
%td
Go to repository charts
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
......
!!! 5 !!! 5
%html{ lang: "en", class: "#{page_class}" } %html{ lang: "en", class: "#{page_class}" }
= render "layouts/head" = render "layouts/head"
%body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data = Gon::Base.render_data
= render "layouts/header/default", title: header_title = render "layouts/header/default", title: header_title
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search) = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm.visible-xs %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
- if current_user - if current_user
......
...@@ -71,18 +71,30 @@ ...@@ -71,18 +71,30 @@
%span %span
Snippets Snippets
-# Global shortcut to network page for compatibility -# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
%span
Activity
-# Shortcut to Repository > Graph (formerly, Network)
- if project_nav_tab? :network - if project_nav_tab? :network
%li.hidden %li.hidden
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
Network Graph
-# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- unless @project.empty_repo?
%li.hidden
= link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
Charts
-# Shortcut to create a new issue -# Shortcut to Issues > New Issue
%li.hidden %li.hidden
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do
Create a new issue Create a new issue
-# Shortcut to builds page -# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds - if project_nav_tab? :builds
%li.hidden %li.hidden
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
%p %p
Changing your username will change path to all personal projects! Changing your username will change path to all personal projects!
.col-lg-9 .col-lg-9
= form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f| = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group .form-group
= f.label :username, "Path", class: "label-light" = f.label :username, "Path", class: "label-light"
.input-group .input-group
......
- if @user.valid?
:plain
new Flash("Username successfully changed", "notice")
- else
- error = @user.errors.full_messages.first
:plain
new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
%span.label.label-info triggered %span.label.label-info triggered
- if build.try(:allow_failure) - if build.try(:allow_failure)
%span.label.label-danger allowed to fail %span.label.label-danger allowed to fail
- if build.manual? - if build.action?
%span.label.label-info manual %span.label.label-info manual
- if pipeline_link - if pipeline_link
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue = render 'shared/issuable/form', f: f, issuable: @issue
:javascript
$('.assign-to-me-link').on('click', function(e){
$('#issue_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
...@@ -2,16 +2,15 @@ ...@@ -2,16 +2,15 @@
This service will be installed on the Mattermost instance at This service will be installed on the Mattermost instance at
%strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
%hr %hr
= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| = form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f|
%h4 Team %h4 Team
%p %p
= @teams.one? ? 'The team' : 'Select the team' = @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in where the slash commands will be used in
- selected_id = @teams.one? ? @teams.keys.first : 0 - selected_id = @teams.one? ? @teams.first['id'] : nil
- options = mattermost_teams_options(@teams) - options = options_for_select(mattermost_teams_options(@teams), selected_id)
- options = options_for_select(options, selected_id) = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
= f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
= f.hidden_field(:team_id, value: selected_id) if @teams.one?
.help-block .help-block
- if @teams.one? - if @teams.one?
This is the only available team. This is the only available team.
...@@ -25,7 +24,7 @@ ...@@ -25,7 +24,7 @@
%hr %hr
%h4 Command trigger word %h4 Command trigger word
%p Choose the word that will trigger commands %p Choose the word that will trigger commands
= f.text_field(:trigger, value: @project.path, class: 'form-control') = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
.help-block .help-block
%p %p
Trigger word must be unique, and can't begin with a slash or contain any spaces. Trigger word must be unique, and can't begin with a slash or contain any spaces.
......
- @body_class = 'card-content'
.service-installation .service-installation
.inline.pull-right .inline.pull-right
= custom_icon('mattermost_logo', size: 48) = custom_icon('mattermost_logo', size: 48)
......
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request = render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
$('.assign-to-me-link').on('click', function(e){
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
...@@ -51,11 +51,6 @@ ...@@ -51,11 +51,6 @@
.mr-loading-status .mr-loading-status
= spinner = spinner
:javascript
$('.assign-to-me-link').on('click', function(e){
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
:javascript :javascript
var merge_request = new MergeRequest({ var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
......
...@@ -29,9 +29,9 @@ ...@@ -29,9 +29,9 @@
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal .normal
%span Request to merge %span <b>Request to merge</b>
%span.label-branch= source_branch_with_namespace(@merge_request) %span.label-branch= source_branch_with_namespace(@merge_request)
%span into %span <b>into</b>
%span.label-branch %span.label-branch
= link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch? - if @merge_request.open? && @merge_request.diverged_from_target_branch?
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.mr-widget-heading .mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status| - %w[success success_with_warnings skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon-#{status}" } %div{ class: "ci-status-icon ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status) = ci_icon_for_status(status)
%span %span
......
...@@ -7,28 +7,46 @@ ...@@ -7,28 +7,46 @@
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- if !@merge_request.source_branch_exists? || params[:deleted_source_branch] - if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
%p .remove-message-pipes
The changes were merged into %ul
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. %li
The source branch has been removed. %span
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
%li
%span
The source branch has been removed.
= render 'projects/merge_requests/widget/merged_buttons' = render 'projects/merge_requests/widget/merged_buttons'
- elsif @merge_request.can_remove_source_branch?(current_user) - elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget .remove_source_branch_widget.remove-message-pipes
%p %ul
The changes were merged into %li
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. %span
You can remove the source branch now. The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
%li
%span
You can remove the source branch now.
= render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
.remove_source_branch_widget.failed.hide .remove_source_branch_widget.failed.remove-message-pipes.hide
%p %ul
Failed to remove source branch '#{@merge_request.source_branch}'. %li
%span
.remove_source_branch_in_progress.hide Failed to remove source branch '#{@merge_request.source_branch}'.
%p .remove_source_branch_in_progress.remove-message-pipes.hide
= icon('spinner spin') %ul
Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. %li
%span
= icon('spinner spin')
Removing source branch '#{@merge_request.source_branch}'.
%li
%span
Please wait, this page will be automatically reloaded.
- else - else
%p .remove-message-pipes
The changes were merged into %ul
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. %li
= render 'projects/merge_requests/widget/merged_buttons' %span
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
= render 'projects/merge_requests/widget/merged_buttons'
...@@ -9,6 +9,6 @@ ...@@ -9,6 +9,6 @@
= icon('trash-o') = icon('trash-o')
Remove Source Branch Remove Source Branch
- if mr_can_be_reverted - if mr_can_be_reverted
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close")
- if mr_can_be_cherry_picked - if mr_can_be_cherry_picked
= cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
...@@ -3,20 +3,24 @@ ...@@ -3,20 +3,24 @@
- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user) - can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
%h4.has-conflicts %h4.has-conflicts
= icon("exclamation-triangle") %p
This merge request contains merge conflicts = icon("exclamation-triangle")
This merge request contains merge conflicts
%p .remove-message-pipes
To merge this request, resolve these conflicts %ul
- if can_resolve && !can_resolve_in_ui %li
locally %span
or To merge this request, resolve these conflicts
- unless can_merge - if can_resolve && !can_resolve_in_ui
ask someone with write access to this repository to locally
merge it locally. or
- unless can_merge
ask someone with write access to this repository to
merge it locally.
- if (can_resolve && can_resolve_in_ui) || can_merge - if (can_resolve && can_resolve_in_ui) || can_merge
.btn-group .merged-buttons.clearfix
- if can_resolve && can_resolve_in_ui - if can_resolve && can_resolve_in_ui
= link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn" = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
- if can_merge - if can_merge
......
...@@ -4,20 +4,20 @@ ...@@ -4,20 +4,20 @@
%h4 %h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the pipeline succeeds. to be merged automatically when the pipeline succeeds.
%div .remove-message-pipes
%p %ul
= succeed '.' do %li
The changes will be %span
- if @merge_request.squash = succeed '.' do
squashed and The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}
- if @project.merge_requests_ff_only_enabled - if @merge_request.remove_source_branch?
fast-forward %li
merged into %span
%span.label-branch= @merge_request.target_branch The source branch will be removed.
- if @merge_request.remove_source_branch? - else
The source branch will be removed. %li
- else %span
The source branch will not be removed. The source branch will not be removed.
- remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
- user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
......
- page_title "Graph", @ref - page_title "Graph", @ref
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/raphael.js')
= page_specific_javascript_bundle_tag('network') = page_specific_javascript_bundle_tag('network')
= render "projects/commits/head" = render "projects/commits/head"
= render "head" = render "head"
......
...@@ -23,4 +23,4 @@ ...@@ -23,4 +23,4 @@
to post a comment to post a comment
:javascript :javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
...@@ -77,8 +77,9 @@ ...@@ -77,8 +77,9 @@
Set up auto deploy Set up auto deploy
- if @repository.commit - if @repository.commit
.project-last-commit{ class: container_class } %div{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project .project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class } %div{ class: container_class }
- if @project.archived? - if @project.archived?
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control', = f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true, autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
title: 'Please choose a group name with no special characters.' title: 'Please choose a group name with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent - if parent
= f.hidden_field :parent_id, value: parent.id = f.hidden_field :parent_id, value: parent.id
......
...@@ -45,11 +45,11 @@ ...@@ -45,11 +45,11 @@
- if current_user && defined?(@project) - if current_user && defined?(@project)
.label-subscription.inline .label-subscription.inline
- if label.is_a?(ProjectLabel) - if label.is_a?(ProjectLabel)
%button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
%span= label_subscription_toggle_button_text(label, @project) %span= label_subscription_toggle_button_text(label, @project)
= icon('spinner spin', class: 'label-subscribe-button-loading') = icon('spinner spin', class: 'label-subscribe-button-loading')
- else - else
%button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } } %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
%span Unsubscribe %span Unsubscribe
= icon('spinner spin', class: 'label-subscribe-button-loading') = icon('spinner spin', class: 'label-subscribe-button-loading')
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M250.05 34c1.9.04 3.8.11 5.6.2l-29.79 35.51c-.07.01-.15.03-.23.04C149.26 84.1 98.22 146.5 98.22 222.97c0 41.56 23.07 90.5 59.75 119.1 28.61 22.32 64.29 36.9 101.21 36.9 93.4 0 160.15-68.61 160.15-156 0-34.91-15.99-72.77-41.76-100.76l-1.63-47.39c54.45 39.15 89.95 103.02 90.06 175.17v.01c0 119.29-96.7 216-216 216-119.29 0-216-96.71-216-216S130.71 34 250 34h.05zm64.1 20.29c.66-.04 1.32.03 1.96.25 3.01 1 3.85 3.57 3.93 6.45l3.84 146.88c.76 28.66-17.16 68.44-60.39 68.56-30.97.08-63.68-20.83-63.68-60.13.01-14.73 5.61-31.26 19.25-48.11l90.03-111.18c1.15-1.42 3.08-2.58 5.06-2.72z"/></svg>
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) } .col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder .issuable-form-select-holder
- if issuable.assignee_id = form.hidden_field :assignee_id
= form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
.form-group.issue-milestone .form-group.issue-milestone
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) } .col-sm-10{ class: ("col-lg-8" if has_due_date) }
......
class StuckCiBuildsWorker
include Sidekiq::Worker
include CronjobQueue
BUILD_STUCK_TIMEOUT = 1.day
def perform
return if Gitlab::Geo.secondary?
Rails.logger.info 'Cleaning stuck builds'
builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
builds.find_each(batch_size: 50).each do |build|
Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
build.drop
end
# Update builds that failed to drop
builds.update_all(status: 'failed')
end
end
class StuckCiJobsWorker
include Sidekiq::Worker
include CronjobQueue
EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'.freeze
BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
BUILD_PENDING_STUCK_TIMEOUT = 1.hour
def perform
return unless try_obtain_lease
Rails.logger.info "#{self.class}: Cleaning stuck builds"
drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT
drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT
drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT
remove_lease
end
private
def try_obtain_lease
@uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
end
def remove_lease
Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
end
def drop(status, timeout)
search(status, timeout) do |build|
drop_build :outdated, build, status, timeout
end
end
def drop_stuck(status, timeout)
search(status, timeout) do |build|
return unless build.stuck?
drop_build :stuck, build, status, timeout
end
end
def search(status, timeout)
builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
yield(build)
end
end
def drop_build(type, build, status, timeout)
Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
b.drop
end
end
end
class UploadChecksumWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(upload_id)
upload = Upload.find(upload_id)
upload.calculate_checksum
upload.save!
rescue ActiveRecord::RecordNotFound
Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping")
end
end
---
title: Remove remnants of git annex support.
merge_request:
author:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment