Commit 4256c774 authored by James Lopez's avatar James Lopez

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee

parents 3d91cfa5 2d5901c4
...@@ -229,7 +229,7 @@ gem 'oj', '~> 2.17.4' ...@@ -229,7 +229,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2' gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
gem 'sassc-rails', '~> 1.3.0' gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
...@@ -263,7 +263,7 @@ end ...@@ -263,7 +263,7 @@ end
group :development do group :development do
gem 'foreman', '~> 0.78.0' gem 'foreman', '~> 0.78.0'
gem 'brakeman', '~> 3.3.0', require: false gem 'brakeman', '~> 3.4.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.2.0', require: false gem 'bullet', '~> 5.2.0', require: false
......
...@@ -88,7 +88,7 @@ GEM ...@@ -88,7 +88,7 @@ GEM
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.3.2) brakeman (3.4.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.2) builder (3.2.2)
bullet (5.2.0) bullet (5.2.0)
...@@ -691,17 +691,12 @@ GEM ...@@ -691,17 +691,12 @@ GEM
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.22) sass (3.4.22)
sassc (1.11.1) sass-rails (5.0.6)
bundler railties (>= 4.0.0, < 6)
ffi (~> 1.9.6) sass (~> 3.1)
sass (>= 3.3.0) sprockets (>= 2.8, < 4.0)
sassc-rails (1.3.0) sprockets-rails (>= 2.0, < 4.0)
railties (>= 4.0.0) tilt (>= 1.1, < 3)
sass
sassc (~> 1.9)
sprockets (> 2.11)
sprockets-rails
tilt
sawyer (0.8.1) sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6) addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0) faraday (~> 0.8, < 1.0)
...@@ -876,7 +871,7 @@ DEPENDENCIES ...@@ -876,7 +871,7 @@ DEPENDENCIES
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0) brakeman (~> 3.4.0)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.2.0) bullet (~> 5.2.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
...@@ -1009,7 +1004,7 @@ DEPENDENCIES ...@@ -1009,7 +1004,7 @@ DEPENDENCIES
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rugged (~> 0.24.0)
sanitize (~> 2.0) sanitize (~> 2.0)
sassc-rails (~> 1.3.0) sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign, no-new */
/* global Vue */ /* global Vue */
/* global EnvironmentsService */ /* global EnvironmentsService */
/* global Flash */
//= require vue //= require vue
//= require vue-resource //= require vue-resource
...@@ -10,41 +11,6 @@ ...@@ -10,41 +11,6 @@
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*/
const filterState = state => environment => environment.state === state && environment;
/**
* Given the filter function and the array of environments will return only
* the environments that match the state provided to the filter function.
*
* @param {Function} fn
* @param {Array} array
* @return {Array}
*/
const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
if (item.children) {
const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return fn(item);
}).filter(Boolean);
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: { props: {
store: { store: {
...@@ -81,10 +47,6 @@ ...@@ -81,10 +47,6 @@
}, },
computed: { computed: {
filteredEnvironments() {
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
},
scope() { scope() {
return this.$options.getQueryParameter('scope'); return this.$options.getQueryParameter('scope');
}, },
...@@ -111,7 +73,7 @@ ...@@ -111,7 +73,7 @@
const scope = this.$options.getQueryParameter('scope'); const scope = this.$options.getQueryParameter('scope');
if (scope) { if (scope) {
this.visibility = scope; this.store.storeVisibility(scope);
} }
this.isLoading = true; this.isLoading = true;
...@@ -121,6 +83,10 @@ ...@@ -121,6 +83,10 @@
.then((json) => { .then((json) => {
this.store.storeEnvironments(json); this.store.storeEnvironments(json);
this.isLoading = false; this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
}); });
}, },
...@@ -188,7 +154,7 @@ ...@@ -188,7 +154,7 @@
<div class="blank-state blank-state-no-icon" <div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0"> v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title"> <h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now. You don't have any environments right now.
</h2> </h2>
<p class="blank-state-text"> <p class="blank-state-text">
...@@ -202,13 +168,13 @@ ...@@ -202,13 +168,13 @@
<a <a
v-if="canCreateEnvironmentParsed" v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath" :href="newEnvironmentPath"
class="btn btn-create"> class="btn btn-create js-new-environment-button">
New Environment New Environment
</a> </a>
</div> </div>
<div class="table-holder" <div class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments"> <table class="table ci-table environments">
<thead> <thead>
<tr> <tr>
...@@ -221,7 +187,7 @@ ...@@ -221,7 +187,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="model in filteredEnvironments" <template v-for="model in state.filteredEnvironments"
v-bind:model="model"> v-bind:model="model">
<tr <tr
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
this.state.environments = []; this.state.environments = [];
this.state.stoppedCounter = 0; this.state.stoppedCounter = 0;
this.state.availableCounter = 0; this.state.availableCounter = 0;
this.state.visibility = 'available';
this.state.filteredEnvironments = [];
return this; return this;
}, },
...@@ -59,7 +61,7 @@ ...@@ -59,7 +61,7 @@
if (occurs.length) { if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment); acc[acc.indexOf(occurs[0])].children.push(environment);
acc[acc.indexOf(occurs[0])].children.sort(this.sortByName); acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else { } else {
acc.push({ acc.push({
name: environment.environment_type, name: environment.environment_type,
...@@ -73,13 +75,70 @@ ...@@ -73,13 +75,70 @@
} }
return acc; return acc;
}, []).sort(this.sortByName); }, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree; this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
return environmentsTree; return environmentsTree;
}, },
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item);
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/** /**
* Toggles folder open property given the environment type. * Toggles folder open property given the environment type.
* *
......
...@@ -48,8 +48,9 @@ ...@@ -48,8 +48,9 @@
}, },
DefaultOptions: { DefaultOptions: {
sorter: function(query, items, searchKey) { sorter: function(query, items, searchKey) {
this.setting.highlightFirst = query.length > 0; this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
if (gl.GfmAutoComplete.isLoading(items)) { if (gl.GfmAutoComplete.isLoading(items)) {
this.setting.highlightFirst = false;
return items; return items;
} }
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
* The container should be the table element. * The container should be the table element.
* *
* The stage icon clicked needs to have the following HTML structure: * The stage icon clicked needs to have the following HTML structure:
* <div> * <div class="dropdown">
* <button class="dropdown js-builds-dropdown-button"></button> * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
* <div class="js-builds-dropdown-container"></div> * <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div> * </div>
*/ */
(() => { (() => {
...@@ -26,13 +26,11 @@ ...@@ -26,13 +26,11 @@
} }
/** /**
* Adds and removes the event listener. * Adds the event listener when the dropdown is opened.
* All dropdown events are fired at the .dropdown-menu's parent element.
*/ */
bindEvents() { bindEvents() {
const dropdownButtonSelector = 'button.js-builds-dropdown-button'; $(this.container).on('shown.bs.dropdown', this.getBuildsList);
$(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
.on('click', dropdownButtonSelector, this.getBuildsList);
} }
/** /**
...@@ -52,11 +50,14 @@ ...@@ -52,11 +50,14 @@
/** /**
* For the clicked stage, gets the list of builds. * For the clicked stage, gets the list of builds.
* *
* @param {Object} e * All dropdown events have a relatedTarget property,
* whose value is the toggling anchor element.
*
* @param {Object} e bootstrap dropdown event
* @return {Promise} * @return {Promise}
*/ */
getBuildsList(e) { getBuildsList(e) {
const button = e.currentTarget; const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint; const endpoint = button.dataset.stageEndpoint;
return $.ajax({ return $.ajax({
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/* global GLForm */ /* global GLForm */
/* global Autosave */ /* global Autosave */
/* global ResolveService */ /* global ResolveService */
/* global mrRefreshWidgetUrl */
/*= require autosave */ /*= require autosave */
/*= require autosize */ /*= require autosize */
...@@ -244,6 +245,16 @@ ...@@ -244,6 +245,16 @@
}; };
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
}
if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
$.get(mrRefreshWidgetUrl);
}
};
/* /*
Render note in main comments area. Render note in main comments area.
...@@ -429,6 +440,7 @@ ...@@ -429,6 +440,7 @@
*/ */
Notes.prototype.addNote = function(xhr, note, status) { Notes.prototype.addNote = function(xhr, note, status) {
this.handleCreateChanges(note);
return this.renderNote(note); return this.renderNote(note);
}; };
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
// //
(function () { (function () {
var lastTextareaPreviewed; var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview; var markdownPreview;
var previewButtonSelector; var previewButtonSelector;
var writeButtonSelector; var writeButtonSelector;
...@@ -104,10 +105,14 @@ ...@@ -104,10 +105,14 @@
if (!$form) { if (!$form) {
return; return;
} }
lastTextareaPreviewed = $form.find('textarea.markdown-area'); lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active'); $form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().addClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show(); $form.find('.md-preview-holder').show();
...@@ -119,9 +124,15 @@ ...@@ -119,9 +124,15 @@
return; return;
} }
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
if (lastTextareaHeight) {
$form.find('textarea.markdown-area').height(lastTextareaHeight);
}
// toggle tabs // toggle tabs
$form.find(writeButtonSelector).parent().addClass('active'); $form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().removeClass('active');
// toggle content // toggle content
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
......
...@@ -15,11 +15,21 @@ ...@@ -15,11 +15,21 @@
return $('.save-project-loader').show(); return $('.save-project-loader').show();
}; };
})(this)); })(this));
this.initVisibilitySelect();
this.toggleSettings(); this.toggleSettings();
this.toggleSettingsOnclick(); this.toggleSettingsOnclick();
this.toggleRepoVisibility(); this.toggleRepoVisibility();
} }
ProjectNew.prototype.initVisibilitySelect = function() {
const visibilityContainer = document.querySelector('.js-visibility-select');
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init();
};
ProjectNew.prototype.toggleSettings = function() { ProjectNew.prototype.toggleSettings = function() {
var self = this; var self = this;
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
return function(response) { return function(response) {
var error; var error;
if (response.errorCode) { if (response.errorCode) {
error = new U2FError(response.errorCode); error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error); return _this.renderError(error);
} else { } else {
return _this.renderAuthenticated(JSON.stringify(response)); return _this.renderAuthenticated(JSON.stringify(response));
......
...@@ -5,21 +5,21 @@ ...@@ -5,21 +5,21 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() { this.U2FError = (function() {
function U2FError(errorCode) { function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode; this.errorCode = errorCode;
this.message = bind(this.message, this); this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:'; this.httpsDisabled = window.location.protocol !== 'https:';
this.u2fFlowType = u2fFlowType;
} }
U2FError.prototype.message = function() { U2FError.prototype.message = function() {
switch (false) { if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
return "This device has already been registered with us."; if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
default:
return "There was a problem communicating with your device.";
} }
return "There was a problem communicating with your device.";
}; };
return U2FError; return U2FError;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
return function(response) { return function(response) {
var error; var error;
if (response.errorCode) { if (response.errorCode) {
error = new U2FError(response.errorCode); error = new U2FError(response.errorCode, 'register');
return _this.renderError(error); return _this.renderError(error);
} else { } else {
return _this.renderRegistered(JSON.stringify(response)); return _this.renderRegistered(JSON.stringify(response));
......
(() => {
const gl = window.gl || (window.gl = {});
class VisibilitySelect {
constructor(container) {
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
this.container = container;
this.helpBlock = this.container.querySelector('.help-block');
this.select = this.container.querySelector('select');
}
init() {
if (this.select) {
this.updateHelpText();
this.select.addEventListener('change', this.updateHelpText.bind(this));
} else {
this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
}
}
updateHelpText() {
this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
}
}
gl.VisibilitySelect = VisibilitySelect;
})();
...@@ -5,18 +5,19 @@ ...@@ -5,18 +5,19 @@
gl.VueStage = Vue.extend({ gl.VueStage = Vue.extend({
data() { data() {
return { return {
count: 0,
builds: '', builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>', spinner: '<span class="fa fa-spinner fa-spin"></span>',
}; };
}, },
props: ['stage', 'svgs', 'match'], props: ['stage', 'svgs', 'match'],
methods: { methods: {
fetchBuilds() { fetchBuilds(e) {
if (this.count > 0) return null; const areaExpanded = e.currentTarget.attributes['aria-expanded'];
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path) return this.$http.get(this.stage.dropdown_path)
.then((response) => { .then((response) => {
this.count += 1;
this.builds = JSON.parse(response.body).html; this.builds = JSON.parse(response.body).html;
}, () => { }, () => {
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
...@@ -39,7 +40,7 @@ ...@@ -39,7 +40,7 @@
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
}, },
svg() { svg() {
const icon = this.stage.status.icon; const { icon } = this.stage.status;
const stageIcon = icon.replace(/icon/i, 'stage_icon'); const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)]; return this.svgs[this.match(stageIcon)];
}, },
...@@ -50,18 +51,25 @@ ...@@ -50,18 +51,25 @@
template: ` template: `
<div> <div>
<button <button
@click='fetchBuilds' @click='fetchBuilds($event)'
:class="triggerButtonClass" :class="triggerButtonClass"
:title='stage.title' :title='stage.title'
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
type="button"> type="button"
>
<span v-html="svg"></span> <span v-html="svg"></span>
<i class="fa fa-caret-down "></i> <i class="fa fa-caret-down "></i>
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div> <div class="arrow-up"></div>
<div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div> <div
@click=''
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner"
>
</div>
</ul> </ul>
</div> </div>
`, `,
......
...@@ -324,7 +324,7 @@ ...@@ -324,7 +324,7 @@
&:focus { &:focus {
cursor: text; cursor: text;
box-shadow: none; box-shadow: none;
border-color: $border-color; border-color: lighten($dropdown-input-focus-border, 20%);
color: $gray-darkest; color: $gray-darkest;
background-color: $gray-light; background-color: $gray-light;
} }
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
.filter-dropdown { .filter-dropdown {
max-height: 215px; max-height: 215px;
overflow-x: scroll; overflow: auto;
} }
.filter-dropdown-item { .filter-dropdown-item {
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
text-align: left; text-align: left;
padding: 8px 16px; padding: 8px 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow-y: hidden; overflow: hidden;
border-radius: 0; border-radius: 0;
.fa { .fa {
......
...@@ -236,9 +236,13 @@ header.header-sidebar-pinned { ...@@ -236,9 +236,13 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; padding-right: $gutter_width;
.merge-request-tabs-holder.affix { &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width; right: $gutter_width;
} }
&.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
}
} }
&.with-overlay { &.with-overlay {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
max-width: 100%; max-width: 100%;
} }
*:first-child { *:first-child:not(.katex-display) {
margin-top: 0; margin-top: 0;
} }
......
...@@ -515,7 +515,6 @@ ul.notes { ...@@ -515,7 +515,6 @@ ul.notes {
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-top: -1px;
margin-left: -4px; margin-left: -4px;
} }
......
...@@ -44,8 +44,8 @@ ...@@ -44,8 +44,8 @@
.pipeline-info, .pipeline-info,
.pipeline-commit, .pipeline-commit,
.pipeline-actions, .pipeline-stages,
.pipeline-stages { .pipeline-actions {
width: 20%; width: 20%;
} }
} }
...@@ -185,6 +185,7 @@ ...@@ -185,6 +185,7 @@
.stage-cell { .stage-cell {
font-size: 0; font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg, > .stage-container > div > button > span > svg,
> .stage-container > button > svg { > .stage-container > button > svg {
...@@ -202,8 +203,8 @@ ...@@ -202,8 +203,8 @@
position: relative; position: relative;
margin-right: 6px; margin-right: 6px;
.tooltip { .tooltip-inner {
white-space: nowrap; padding: 3px 4px;
} }
&:not(:last-child) { &:not(:last-child) {
...@@ -348,6 +349,7 @@ ...@@ -348,6 +349,7 @@
padding: $gl-padding; padding: $gl-padding;
white-space: nowrap; white-space: nowrap;
transition: max-height 0.3s, padding 0.3s; transition: max-height 0.3s, padding 0.3s;
overflow: auto;
.stage-column-list, .stage-column-list,
.builds-container > ul { .builds-container > ul {
......
...@@ -9,17 +9,17 @@ ...@@ -9,17 +9,17 @@
.new_project, .new_project,
.edit-project { .edit-project {
fieldset { .sharing-and-permissions {
.header {
&.features { padding-top: $gl-vert-padding;
}
.label-light { .label-light {
margin-bottom: 0; margin-bottom: 0;
} }
.help-block { .help-block {
margin-top: 0; margin-top: 0;
}
} }
.form-group { .form-group {
...@@ -198,7 +198,7 @@ ...@@ -198,7 +198,7 @@
margin: 15px 5px 0 0; margin: 15px 5px 0 0;
input { input {
height: 28px; height: 27px;
} }
} }
...@@ -938,10 +938,18 @@ a.allowed-to-push { ...@@ -938,10 +938,18 @@ a.allowed-to-push {
} }
} }
.project-feature-nested { .project-feature {
padding-top: 10px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-left: 45px; padding-left: 45px;
} }
&.nested {
@media (min-width: $screen-sm-min) {
padding-left: 90px;
}
}
} }
.project-repo-select { .project-repo-select {
......
...@@ -5,7 +5,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -5,7 +5,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def update def update
if @application_setting.update_attributes(application_setting_params) result = ::ApplicationSettings::UpdateService.new(@application_setting, current_user, application_setting_params).execute
if result[:status] == :success
redirect_to admin_application_settings_path, redirect_to admin_application_settings_path,
notice: 'Application settings saved successfully' notice: 'Application settings saved successfully'
else else
......
...@@ -18,7 +18,7 @@ class AutocompleteController < ApplicationController ...@@ -18,7 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank? if params[:search].blank?
# Include current user if available to filter by "Me" # Include current user if available to filter by "Me"
if params[:current_user].present? && current_user if params[:current_user].present? && current_user
@users = [*@users, current_user] @users = [current_user, *@users]
end end
if params[:author_id].present? if params[:author_id].present?
......
...@@ -136,7 +136,7 @@ module LfsRequest ...@@ -136,7 +136,7 @@ module LfsRequest
size_of_objects = objects.sum { |o| o[:size] } size_of_objects = objects.sum { |o| o[:size] }
@limit_exceeded = (project.repository_and_lfs_size + size_of_objects.to_mb) > project.actual_size_limit @limit_exceeded = (project.repository_and_lfs_size + size_of_objects) > project.actual_size_limit
end end
end end
......
...@@ -19,11 +19,11 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController ...@@ -19,11 +19,11 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private private
def milestones def milestones
@milestones = GlobalMilestone.build_collection(@projects, params) @milestones = DashboardMilestone.build_collection(@projects, params)
end end
def milestone def milestone
@milestone = GlobalMilestone.build(@projects, params[:title]) @milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone render_404 unless @milestone
end end
end end
...@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController
private private
def build def build
@build ||= project.builds.find_by!(id: params[:id]) @build ||= project.builds.find_by!(id: params[:id]).present(user: current_user)
end end
def build_path(build) def build_path(build)
......
...@@ -12,7 +12,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -12,7 +12,6 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_pipeline!, only: [:pipelines] before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
before_action :define_status_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path] before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
...@@ -106,10 +105,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -106,10 +105,6 @@ class Projects::CommitController < Projects::ApplicationController
} }
end end
def define_status_vars
@ci_pipelines = project.pipelines.where(sha: commit.sha)
end
def assign_change_commit_vars(mr_source_branch) def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id]) @commit = project.commit(params[:id])
@target_branch = params[:target_branch] @target_branch = params[:target_branch]
......
...@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController
end end
def create def create
redirect_to namespace_project_compare_path(@project.namespace, @project, if params[:from].blank? || params[:to].blank?
flash[:alert] = "You must select from and to branches"
from_to_vars = {
from: params[:from].presence,
to: params[:to].presence
}
redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars)
else
redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to]) params[:from], params[:to])
end
end end
private private
......
...@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings" layout "project_settings"
def index
@hooks = @project.hooks
@hook = ProjectHook.new
end
def create def create
@hook = @project.hooks.new(hook_params) @hook = @project.hooks.new(hook_params)
@hook.save @hook.save
if @hook.valid? unless @hook.valid?
redirect_to namespace_project_hooks_path(@project.namespace, @project)
else
@hooks = @project.hooks.select(&:persisted?) @hooks = @project.hooks.select(&:persisted?)
render :index flash[:alert] = @hook.errors.full_messages.join.html_safe
end end
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
def test def test
...@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy def destroy
hook.destroy hook.destroy
redirect_to namespace_project_hooks_path(@project.namespace, @project) redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
private private
......
...@@ -373,6 +373,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -373,6 +373,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
RebaseWorker.perform_async(@merge_request.id, current_user.id) RebaseWorker.perform_async(@merge_request.id, current_user.id)
end end
def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
@status = :success
elsif merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds
end
render 'merge'
end
def branch_from def branch_from
# This is always source # This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project @source_project = @merge_request.nil? ? @project : @merge_request.source_project
......
...@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end end
def create def create
@note = Notes::CreateService.new(project, current_user, note_params).execute create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note) if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user) Banzai::NoteRenderer.render([@note], @project, current_user)
......
...@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings" layout "project_settings"
def index
@services = @project.find_or_initialize_services
end
def edit def edit
end end
......
module Projects
module Settings
class IntegrationsController < Projects::ApplicationController
include ServiceParams
before_action :authorize_admin_project!
layout "project_settings"
def show
@hooks = @project.hooks
@hook = ProjectHook.new
# Services
@services = @project.find_or_initialize_services
end
end
end
end
...@@ -165,4 +165,10 @@ module DiffHelper ...@@ -165,4 +165,10 @@ module DiffHelper
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end end
def render_overflow_warning?(diff_files)
diffs = @merge_request_diff.presence || diff_files
diffs.overflow?
end
end end
...@@ -208,6 +208,10 @@ module GitlabRoutingHelper ...@@ -208,6 +208,10 @@ module GitlabRoutingHelper
end end
# Settings # Settings
def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args)
end
def project_settings_members_path(project, *args) def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args) namespace_project_settings_members_path(project.namespace, project, *args)
end end
......
...@@ -19,6 +19,14 @@ module MergeRequestsHelper ...@@ -19,6 +19,14 @@ module MergeRequestsHelper
} }
end end
def mr_widget_refresh_url(mr)
if mr && mr.source_project
merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
else
''
end
end
def mr_css_classes(mr) def mr_css_classes(mr)
classes = "merge-request" classes = "merge-request"
classes << " closed" if mr.closed? classes << " closed" if mr.closed?
......
...@@ -436,4 +436,15 @@ module ProjectsHelper ...@@ -436,4 +436,15 @@ module ProjectsHelper
def project_issues(project) def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute IssuesFinder.new(current_user, project_id: project.id).execute
end end
def visibility_select_options(project, selected_level)
levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
[
visibility_level_label(level),
{ data: { description: visibility_level_description(level, project) } },
level
]
end
options_for_select(levels_options_array, selected_level)
end
end end
...@@ -11,9 +11,10 @@ module TodosHelper ...@@ -11,9 +11,10 @@ module TodosHelper
case todo.action case todo.action
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for' when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for' when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
end end
end end
......
...@@ -14,6 +14,55 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -14,6 +14,55 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters [\r\n] # any number of newline characters
}x }x
DEFAULTS_CE = {
after_sign_up_text: nil,
akismet_enabled: false,
container_registry_token_expire_delay: 5,
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values,
koding_enabled: false,
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
plantuml_enabled: false,
plantuml_url: nil,
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
send_user_confirmation_email: false,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
sidekiq_throttling_enabled: false,
sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
two_factor_grace_period: 48,
user_default_external: false
}
DEFAULTS_EE = {
elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost',
elasticsearch_port: ENV['ELASTIC_PORT'] || '9200',
usage_ping_enabled: true
}
DEFAULTS = DEFAULTS_CE.merge(DEFAULTS_EE)
serialize :restricted_visibility_levels serialize :restricted_visibility_levels
serialize :import_sources serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array serialize :disabled_oauth_sign_in_sources, Array
...@@ -176,49 +225,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -176,49 +225,7 @@ class ApplicationSetting < ActiveRecord::Base
end end
def self.create_from_defaults def self.create_from_defaults
create( create(DEFAULTS)
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: Gitlab::ImportSources.values,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
plantuml_enabled: false,
plantuml_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
container_registry_token_expire_delay: 5,
elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost',
elasticsearch_port: ENV['ELASTIC_PORT'] || '9200',
usage_ping_enabled: true,
repository_storages: ['default'],
user_default_external: false,
sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
)
end end
def elasticsearch_host def elasticsearch_host
......
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Build < CommitStatus class Build < CommitStatus
include TokenAuthenticatable include TokenAuthenticatable
include AfterCommitQueue include AfterCommitQueue
include Presentable
prepend EE::Build prepend EE::Build
belongs_to :runner belongs_to :runner
...@@ -92,6 +93,12 @@ module Ci ...@@ -92,6 +93,12 @@ module Ci
end end
state_machine :status do state_machine :status do
after_transition any => [:pending] do |build|
build.run_after_commit do
BuildQueueWorker.perform_async(id)
end
end
after_transition pending: :running do |build| after_transition pending: :running do |build|
build.run_after_commit do build.run_after_commit do
BuildHooksWorker.perform_async(id) BuildHooksWorker.perform_async(id)
...@@ -509,6 +516,10 @@ module Ci ...@@ -509,6 +516,10 @@ module Ci
end end
end end
def has_expiring_artifacts?
artifacts_expire_at.present?
end
def keep_artifacts! def keep_artifacts!
self.update(artifacts_expire_at: nil) self.update(artifacts_expire_at: nil)
end end
......
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked] FORM_EDITABLE = %i[description tag_list active run_untagged locked]
...@@ -21,6 +22,8 @@ module Ci ...@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) } scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
...@@ -122,8 +125,36 @@ module Ci ...@@ -122,8 +125,36 @@ module Ci
] ]
end end
def tick_runner_queue
new_update = SecureRandom.hex
Gitlab::Redis.with { |redis| redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) }
new_update
end
def ensure_runner_queue_value
Gitlab::Redis.with do |redis|
value = SecureRandom.hex
redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
redis.get(runner_queue_key)
end
end
def is_runner_queue_value_latest?(value)
ensure_runner_queue_value == value if value.present?
end
private private
def runner_queue_key
"runner:build_queue:#{self.token}"
end
def form_editable_changed?
FORM_EDITABLE.any? do |editable|
public_send("#{editable}_changed?")
end
end
def tag_constraints def tag_constraints
unless has_tags? || run_untagged? unless has_tags? || run_untagged?
errors.add(:tags_list, errors.add(:tags_list,
......
...@@ -318,6 +318,20 @@ class Commit ...@@ -318,6 +318,20 @@ class Commit
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end end
def persisted?
true
end
def touch
# no-op but needs to be defined since #persisted? is defined
end
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress?
!!(title =~ WIP_REGEX)
end
private private
def commit_reference(from_project, referable_commit_id, full: false) def commit_reference(from_project, referable_commit_id, full: false)
......
...@@ -7,11 +7,14 @@ module Milestoneish ...@@ -7,11 +7,14 @@ module Milestoneish
def total_items_count(user) def total_items_count(user)
memoize_per_user(user, :total_items_count) do memoize_per_user(user, :total_items_count) do
issues_count = count_issues_by_state(user).values.sum total_issues_count(user) + merge_requests.size
issues_count + merge_requests.size
end end
end end
def total_issues_count(user)
count_issues_by_state(user).values.sum
end
def complete?(user) def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end end
......
module Presentable
def present(**attributes)
Gitlab::View::Presenter::Factory
.new(self, attributes)
.fabricate!
end
end
class DashboardMilestone < GlobalMilestone
def issues_finder_params
{ authorized_only: true }
end
end
class GenericCommitStatus < CommitStatus class GenericCommitStatus < CommitStatus
before_validation :set_default_values before_validation :set_default_values
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
# GitHub compatible API # GitHub compatible API
alias_attribute :context, :name alias_attribute :context, :name
...@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus ...@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus
def tags def tags
[:external] [:external]
end end
def detailed_status(current_user)
Gitlab::Ci::Status::External::Factory
.new(self, current_user)
.fabricate!
end
end end
...@@ -96,6 +96,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -96,6 +96,10 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block| around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block) Gitlab::Timeless.timeless(merge_request, &block)
end end
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
TodoService.new.merge_request_became_unmergeable(merge_request)
end
end end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
...@@ -949,10 +953,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -949,10 +953,22 @@ class MergeRequest < ActiveRecord::Base
end end
def has_commits? def has_commits?
commits_count > 0 merge_request_diff && commits_count > 0
end end
def has_no_commits? def has_no_commits?
!has_commits? !has_commits?
end end
def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
return false unless can_be_merged_by?(current_user)
return true if autocomplete_precheck
return false unless mergeable?(skip_ci_check: true)
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
return false if last_diff_sha != diff_head_sha
true
end
end end
...@@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base
# and save it as array of hashes in st_diffs db field # and save it as array of hashes in st_diffs db field
def save_diffs def save_diffs
new_attributes = {} new_attributes = {}
new_diffs = []
if commits.size.zero? if commits.size.zero?
new_attributes[:state] = :empty new_attributes[:state] = :empty
else else
diff_collection = compare.diffs(Commit.max_diff_options) diff_collection = compare.diffs(Commit.max_diff_options)
new_attributes[:real_size] = compare.diffs.real_size
if diff_collection.overflow?
# Set our state to 'overflow' to make the #empty? and #collected?
# methods (generated by StateMachine) return false.
new_attributes[:state] = :overflow
end
new_attributes[:real_size] = diff_collection.real_size
if diff_collection.any? if diff_collection.any?
new_diffs = dump_diffs(diff_collection) new_diffs = dump_diffs(diff_collection)
new_attributes[:state] = :collected new_attributes[:state] = :collected
end end
new_attributes[:st_diffs] = new_diffs || []
# Set our state to 'overflow' to make the #empty? and #collected?
# methods (generated by StateMachine) return false.
#
# This attribution has to come at the end of the method so 'overflow'
# state does not get overridden by 'collected'.
new_attributes[:state] = :overflow if diff_collection.overflow?
end end
new_attributes[:st_diffs] = new_diffs
update_columns_serialized(new_attributes) update_columns_serialized(new_attributes)
end end
......
...@@ -1541,8 +1541,10 @@ class Project < ActiveRecord::Base ...@@ -1541,8 +1541,10 @@ class Project < ActiveRecord::Base
actual_size_limit != 0 actual_size_limit != 0
end end
def changes_will_exceed_size_limit?(size_mb) def changes_will_exceed_size_limit?(size_in_bytes)
size_limit_enabled? && (size_mb > actual_size_limit || size_mb + repository_and_lfs_size > actual_size_limit) size_limit_enabled? &&
(size_in_bytes > actual_size_limit ||
size_in_bytes + repository_and_lfs_size > actual_size_limit)
end end
def environments_for(ref, commit: nil, with_tags: false) def environments_for(ref, commit: nil, with_tags: false)
......
...@@ -29,8 +29,9 @@ class ProjectStatistics < ActiveRecord::Base ...@@ -29,8 +29,9 @@ class ProjectStatistics < ActiveRecord::Base
self.commit_count = project.repository.commit_count self.commit_count = project.repository.commit_count
end end
# Repository#size needs to be converted from MB to Byte.
def update_repository_size def update_repository_size
self.repository_size = project.repository.size self.repository_size = project.repository.size * 1.megabyte
end end
def update_lfs_objects_size def update_lfs_objects_size
......
...@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base ...@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3 BUILD_FAILED = 3
MARKED = 4 MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature APPROVAL_REQUIRED = 5 # This is an EE-only feature
UNMERGEABLE = 6
ACTION_NAMES = { ACTION_NAMES = {
ASSIGNED => :assigned, ASSIGNED => :assigned,
MENTIONED => :mentioned, MENTIONED => :mentioned,
BUILD_FAILED => :build_failed, BUILD_FAILED => :build_failed,
MARKED => :marked, MARKED => :marked,
APPROVAL_REQUIRED => :approval_required APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable
} }
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base ...@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base
end end
end end
def unmergeable?
action == UNMERGEABLE
end
def build_failed? def build_failed?
action == BUILD_FAILED action == BUILD_FAILED
end end
......
...@@ -53,6 +53,10 @@ class BasePolicy ...@@ -53,6 +53,10 @@ class BasePolicy
def self.class_for(subject) def self.class_for(subject)
return GlobalPolicy if subject.nil? return GlobalPolicy if subject.nil?
if subject.class.try(:presenter?)
subject = subject.subject
end
subject.class.ancestors.each do |klass| subject.class.ancestors.each do |klass|
next unless klass.name next unless klass.name
......
# Presenters
This type of class is responsible for giving the view an object which defines
**view-related logic/data methods**. It is usually useful to extract such
methods from models to presenters.
## When to use a presenter?
### When your view is full of logic
When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
time to create a presenter!
### When your model has a lot of view-related logic/data methods
When your model has a lot of view-related logic/data methods, you can easily
move them to a presenter.
## Why are we using presenters instead of helpers?
We don't use presenters to generate complex view output that would rely on helpers.
Presenters should be used for:
- Data and logic methods that can be pulled & combined into single methods from
view. This can include loops extracted from views too. A good example is
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs.
- Data and logic methods that can be pulled from models.
- Simple text output methods: it's ok if the method returns a string, but not a
whole DOM element for which we'd need HAML, a view context, helpers etc.
## Why use presenters instead of model concerns?
We should strive to follow the single-responsibility principle, and view-related
logic/data methods are definitely not the responsibility of models!
Another reason is as follows:
> Avoid using concerns and use presenters instead. Why? After all, concerns seem
to be a core part of Rails and can DRY up code when shared among multiple models.
Nonetheless, the main issue is that concerns don’t make the model object more
cohesive. The code is just better organized. In other words, there’s no real
change to the API of the model.
– https://www.toptal.com/ruby-on-rails/decoupling-rails-components
## Benefits
By moving pure view-related logic/data methods from models & views to presenters,
we gain the following benefits:
- rules are more explicit and centralized in the presenter => improves security
- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
- views are more readable and maintainable
- decreases number of CE -> EE merge conflicts since code is in separate files
- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
## What not to do with presenters?
- Don't use helpers in presenters. Presenters are not aware of the view context.
- Don't generate complex DOM elements, forms etc. with presenters. Presenters
can return simple data as texts, and URLs using URL helpers from
`Gitlab::Routing` but nothing much more fancy.
## Implementation
### Presenter definition
Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
provides a `.presents` method which allows you to define an accessor for the
presented object. It also includes common helpers like `Gitlab::Routing` and
`Gitlab::Allowable`.
```ruby
class LabelPresenter < Gitlab::View::Presenter::Simple
presents :label
def text_color
label.color.to_s
end
def to_partial_path
'projects/labels/show'
end
end
```
In some cases, it can be more practical to transparently delegate all missing
method calls to the presented object, in these cases, you can make your
presenter inherit from `Gitlab::View::Presenter::Delegated`:
```ruby
class LabelPresenter < Gitlab::View::Presenter::Delegated
presents :label
def text_color
# color is delegated to label
color.to_s
end
def to_partial_path
'projects/labels/show'
end
end
```
### Presenter instantiation
Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
detects the presenter based on the presented subject's class.
```ruby
class Projects::LabelsController < Projects::ApplicationController
def edit
@label = Gitlab::View::Presenter::Factory
.new(@label, user: current_user)
.fabricate!
end
end
```
You can also include the `Presentable` concern in the model:
```ruby
class Label
include Presentable
end
```
and then in the controller:
```ruby
class Projects::LabelsController < Projects::ApplicationController
def edit
@label = @label.present(user: current_user)
end
end
```
### Presenter usage
```ruby
%div{ class: @label.text_color }
= render partial: @label, label: @label
```
You can also present the model in the view:
```ruby
- label = @label.present(current_user)
%div{ class: label.text_color }
= render partial: label, label: label
```
module Ci
class BuildPresenter < Gitlab::View::Presenter::Delegated
presents :build
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
erased? && erased_by
end
def erased_by_name
erased_by.name if erased_by_user?
end
end
end
module ApplicationSettings
class BaseService < ::BaseService
attr_accessor :application_setting, :current_user, :params
def initialize(application_setting, user, params = {})
@application_setting, @current_user, @params = application_setting, user, params.dup
end
end
end
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(application_setting)
if application_setting.update(params)
success
else
error('Application settings could not be updated')
end
end
end
end
...@@ -46,6 +46,13 @@ class BaseService ...@@ -46,6 +46,13 @@ class BaseService
private private
def assign_repository_size_limit_as_bytes(model)
repository_size_limit = @params.delete(:repository_size_limit)
new_value = repository_size_limit.to_i.megabytes if repository_size_limit.present?
model.repository_size_limit = new_value
end
def error(message, http_status = nil) def error(message, http_status = nil)
result = { result = {
message: message, message: message,
......
module Ci
class UpdateBuildQueueService
def execute(build)
build.project.runners.each do |runner|
if runner.can_pick?(build)
runner.tick_runner_queue
end
end
end
end
end
...@@ -12,6 +12,9 @@ module Groups ...@@ -12,6 +12,9 @@ module Groups
return @group return @group
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(@group)
if @group.parent && !can?(current_user, :admin_group, @group.parent) if @group.parent && !can?(current_user, :admin_group, @group.parent)
@group.parent = nil @group.parent = nil
@group.errors.add(:parent_id, 'manage access required to create subgroup') @group.errors.add(:parent_id, 'manage access required to create subgroup')
......
...@@ -12,6 +12,9 @@ module Groups ...@@ -12,6 +12,9 @@ module Groups
end end
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(group)
group.assign_attributes(params) group.assign_attributes(params)
begin begin
......
...@@ -21,6 +21,7 @@ module MergeRequests ...@@ -21,6 +21,7 @@ module MergeRequests
end end
comment_mr_with_commits comment_mr_with_commits
mark_mr_as_wip_from_commits
execute_mr_web_hooks execute_mr_web_hooks
reset_approvals_for_merge_requests reset_approvals_for_merge_requests
...@@ -151,6 +152,24 @@ module MergeRequests ...@@ -151,6 +152,24 @@ module MergeRequests
end end
end end
def mark_mr_as_wip_from_commits
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
wip_commit = @commits.detect(&:work_in_progress?)
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
SystemNoteService.add_merge_request_wip_from_commit(
merge_request,
merge_request.project,
@current_user,
wip_commit
)
end
end
end
# Call merge request webhook with update branches # Call merge request webhook with update branches
def execute_mr_web_hooks def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request| merge_requests_for_source_branch.each do |merge_request|
......
...@@ -7,6 +7,8 @@ module MergeRequests ...@@ -7,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id) params.except!(:target_project_id)
params.except!(:source_branch) params.except!(:source_branch)
merge_from_slash_command(merge_request) if params[:merge]
if merge_request.closed_without_fork? if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch) params.except!(:target_branch, :force_remove_source_branch)
end end
...@@ -79,6 +81,19 @@ module MergeRequests ...@@ -79,6 +81,19 @@ module MergeRequests
end end
end end
def merge_from_slash_command(merge_request)
last_diff_sha = params.delete(:merge)
return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
merge_request.update(merge_error: nil)
if merge_request.head_pipeline && merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
else
MergeWorker.perform_async(merge_request.id, current_user.id, {})
end
end
def reopen_service def reopen_service
MergeRequests::ReopenService MergeRequests::ReopenService
end end
......
module Notes module Notes
class CreateService < BaseService class CreateService < BaseService
def execute def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = project.notes.new(params) note = project.notes.new(params)
note.author = current_user note.author = current_user
note.system = false note.system = false
...@@ -19,7 +21,8 @@ module Notes ...@@ -19,7 +21,8 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user) slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note) if slash_commands_service.supported?(note)
content, command_params = slash_commands_service.extract_commands(note) options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
content, command_params = slash_commands_service.extract_commands(note, options)
only_commands = content.empty? only_commands = content.empty?
......
...@@ -19,10 +19,10 @@ module Notes ...@@ -19,10 +19,10 @@ module Notes
self.class.supported?(note, current_user) self.class.supported?(note, current_user)
end end
def extract_commands(note) def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note) return [note.note, {}] unless supported?(note)
SlashCommands::InterpretService.new(project, current_user). SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable) execute(note.note, note.noteable)
end end
......
...@@ -22,6 +22,9 @@ module Projects ...@@ -22,6 +22,9 @@ module Projects
return @project return @project
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(@project)
# Set project name from path # Set project name from path
if @project.name.present? && @project.path.present? if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok # if both name and path set - everything is ok
......
...@@ -13,6 +13,9 @@ module Projects ...@@ -13,6 +13,9 @@ module Projects
end end
end end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(project)
new_branch = params.delete(:default_branch) new_branch = params.delete(:default_branch)
new_repository_storage = params.delete(:repository_storage) new_repository_storage = params.delete(:repository_storage)
......
...@@ -2,7 +2,7 @@ module SlashCommands ...@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl include Gitlab::SlashCommands::Dsl
attr_reader :issuable attr_reader :issuable, :options
# Takes a text and interprets the commands that are extracted from it. # Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record. # Returns the content without commands, and hash of changes to be applied to a record.
...@@ -13,7 +13,8 @@ module SlashCommands ...@@ -13,7 +13,8 @@ module SlashCommands
opts = { opts = {
issuable: issuable, issuable: issuable,
current_user: current_user, current_user: current_user,
project: project project: project,
params: params
} }
content, commands = extractor.extract_commands(content, opts) content, commands = extractor.extract_commands(content, opts)
...@@ -58,6 +59,17 @@ module SlashCommands ...@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen' @updates[:state_event] = 'reopen'
end end
desc 'Merge (when build succeeds)'
condition do
last_diff_sha = params && params[:merge_request_diff_head_sha]
issuable.is_a?(MergeRequest) &&
issuable.persisted? &&
issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
end
command :merge do
@updates[:merge] = params[:merge_request_diff_head_sha]
end
desc 'Change title' desc 'Change title'
params '<New title>' params '<New title>'
condition do condition do
......
...@@ -208,6 +208,12 @@ module SystemNoteService ...@@ -208,6 +208,12 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
def self.resolve_all_discussions(merge_request, project, author) def self.resolve_all_discussions(merge_request, project, author)
body = "resolved all discussions" body = "resolved all discussions"
......
...@@ -98,10 +98,12 @@ class TodoService ...@@ -98,10 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should: # When a build fails on the HEAD of a merge request we should:
# #
# * create a todo for that user to fix it # * create a todo for author of MR to fix it
# * create a todo for merge_user to keep an eye on it
# #
def merge_request_build_failed(merge_request) def merge_request_build_failed(merge_request)
create_build_failed_todo(merge_request) create_build_failed_todo(merge_request, merge_request.author)
create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end end
# When new approvers are added for a merge request: # When new approvers are added for a merge request:
...@@ -123,11 +125,21 @@ class TodoService ...@@ -123,11 +125,21 @@ class TodoService
# When a build is retried to a merge request we should: # When a build is retried to a merge request we should:
# #
# * mark all pending todos related to the merge request for the author as done # * mark all pending todos related to the merge request for the author as done
# * mark all pending todos related to the merge request for the merge_user as done
# #
def merge_request_build_retried(merge_request) def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author) mark_pending_todos_as_done(merge_request, merge_request.author)
mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end end
# When a merge request could not be automatically merged due to its unmergeable state we should:
#
# * create a todo for a merge_user
#
def merge_request_became_unmergeable(merge_request)
create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end
# When create a note we should: # When create a note we should:
# #
# * mark all pending todos related to the noteable for the note author as done # * mark all pending todos related to the noteable for the note author as done
...@@ -254,10 +266,14 @@ class TodoService ...@@ -254,10 +266,14 @@ class TodoService
create_todos(approvers.map(&:user), attributes) create_todos(approvers.map(&:user), attributes)
end end
def create_build_failed_todo(merge_request) def create_build_failed_todo(merge_request, todo_author)
author = merge_request.author attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED) create_todos(todo_author, attributes)
create_todos(author, attributes) end
def create_unmergeable_todo(merge_request, merge_user)
attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE)
create_todos(merge_user, attributes)
end end
def attributes_for_target(target) def attributes_for_target(target)
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
= f.label :repository_size_limit, class: 'control-label col-sm-2' do = f.label :repository_size_limit, class: 'control-label col-sm-2' do
Size limit per repository (MB) Size limit per repository (MB)
.col-sm-10 .col-sm-10
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited. Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings") = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.todo-item.todo-block .todo-item.todo-block
.todo-title.title .todo-title.title
- unless todo.build_failed? - unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo) = todo_target_state_pill(todo)
%span.author-name %span.author-name
......
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
= f.label :repository_size_limit, class: 'control-label' do = f.label :repository_size_limit, class: 'control-label' do
Repository size limit (MB) Repository size limit (MB)
.col-sm-10 .col-sm-10
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
= size_limit_message_for_group(@group) = size_limit_message_for_group(@group)
...@@ -8,14 +8,10 @@ ...@@ -8,14 +8,10 @@
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span %span
Deploy Keys Deploy Keys
= nav_link(controller: :hooks) do = nav_link(controller: :integrations) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span %span
Webhooks Integrations
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
%span
Services
= nav_link(controller: :protected_branches) do = nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span %span
......
- if can_change_visibility_level?(@project, current_user)
= form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select')
- else
.info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
= visibility_level_icon(@project.visibility_level)
%strong
= visibility_level_label(@project.visibility_level)
...@@ -22,14 +22,14 @@ ...@@ -22,14 +22,14 @@
%p.build-detail-row %p.build-detail-row
The artifacts were removed The artifacts were removed
#{time_ago_with_tooltip(@build.artifacts_expire_at)} #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- elsif @build.artifacts_expire_at - elsif @build.has_expiring_artifacts?
%p.build-detail-row %p.build-detail-row
The artifacts will be removed in The artifacts will be removed in
%span.js-artifacts-remove= @build.artifacts_expire_at %span.js-artifacts-remove= @build.artifacts_expire_at
- if @build.artifacts? - if @build.artifacts?
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.artifacts_expire_at - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep Keep
......
...@@ -53,8 +53,10 @@ ...@@ -53,8 +53,10 @@
.prepend-top-default .prepend-top-default
- if @build.erased? - if @build.erased?
.erased.alert.alert-warning .erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - if @build.erased_by_user?
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Build has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else - else
#js-build-scroll.scroll-controls #js-build-scroll.scroll-controls
.scroll-step .scroll-step
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } × %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)} %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body .modal-body
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch .form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label' = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
= nav_link(path: 'commit#pipelines') do = nav_link(path: 'commit#pipelines') do
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines Pipelines
%span.badge= @ci_pipelines.count %span.badge= @commit.pipelines.size
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
= render "commit_box" = render "commit_box"
= render "ci_menu" = render "ci_menu"
= render "pipelines_list", pipelines: @ci_pipelines = render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc)
...@@ -18,8 +18,8 @@ ...@@ -18,8 +18,8 @@
= parallel_diff_btn = parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files = render 'projects/diffs/stats', diff_files: diff_files
- if diff_files.overflow? - if render_overflow_warning?(diff_files)
= render 'projects/diffs/warning', diff_files: diff_files = render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } } .files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file| - diff_files.each_with_index do |diff_file|
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
.form-group .form-group
= f.label :repository_size_limit, class: 'label-light' do = f.label :repository_size_limit, class: 'label-light' do
Repository size limit (MB) Repository size limit (MB)
= f.number_field :repository_size_limit, class: 'form-control', min: 0 = f.number_field :repository_size_limit, value: f.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
= size_limit_message(@project) = size_limit_message(@project)
...@@ -50,55 +50,63 @@ ...@@ -50,55 +50,63 @@
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas. %p.help-block Separate tags with commas.
%hr %hr
%fieldset.features.append-bottom-0 %fieldset.append-bottom-0
%h5.prepend-top-0 %h5.prepend-top-0
Feature Visibility Sharing &amp; Permissions
.form_group.prepend-top-20.sharing-and-permissions
= f.fields_for :project_feature do |feature_fields| .row.js-visibility-select
.form_group.prepend-top-20 .col-md-9
.row %label.label-light
.col-md-9 = label_tag :project_visibility, 'Project Visibility', class: 'label-light'
= feature_fields.label :repository_access_level, "Repository", class: 'label-light' = link_to "(?)", help_page_path("public_access/public_access")
%span.help-block Push files to be stored in this project %span.help-block
.col-md-3.js-repo-access-level .col-md-3.visibility-select-container
= project_feature_access_select(:repository_access_level) = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields|
.col-sm-12 %fieldset.features
.row .row
.col-md-9.project-feature-nested .col-md-9.project-feature
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block Submit changes to be merged upstream %span.help-block View and edit files in this project
.col-md-3 .col-md-3.js-repo-access-level
= project_feature_access_select(:merge_requests_access_level) = project_feature_access_select(:repository_access_level)
.row .row
.col-md-9.project-feature-nested .col-md-9.project-feature.nested
= feature_fields.label :builds_access_level, "Builds", class: 'label-light' = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge %span.help-block Submit changes to be merged upstream
.col-md-3 .col-md-3
= project_feature_access_select(:builds_access_level) = project_feature_access_select(:merge_requests_access_level)
.row .row
.col-md-9 .col-md-9.project-feature.nested
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository %span.help-block Submit, test and deploy your changes before merge
.col-md-3 .col-md-3
= project_feature_access_select(:snippets_access_level) = project_feature_access_select(:builds_access_level)
.row .row
.col-md-9 .col-md-9.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light' = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project %span.help-block Share code pastes with others out of Git repository
.col-md-3 .col-md-3
= project_feature_access_select(:issues_access_level) = project_feature_access_select(:snippets_access_level)
.row .row
.col-md-9 .col-md-9.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Pages for project documentation %span.help-block Lightweight issue tracking system for this project
.col-md-3 .col-md-3
= project_feature_access_select(:wiki_access_level) = project_feature_access_select(:issues_access_level)
.row
.col-md-9.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation
.col-md-3
= project_feature_access_select(:wiki_access_level)
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin? - if Gitlab.config.lfs.enabled && current_user.admin?
.row .row
.col-md-9 .col-md-9
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= link_to 'Change branches', mr_change_branches_path(@merge_request) = link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr %hr
= 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, commits: @commits
= f.hidden_field :source_project_id = f.hidden_field :source_project_id
= f.hidden_field :source_branch = f.hidden_field :source_branch
= f.hidden_field :target_project_id = f.hidden_field :target_project_id
......
...@@ -113,3 +113,6 @@ ...@@ -113,3 +113,6 @@
action: "#{controller.action_name}" action: "#{controller.action_name}"
}); });
}); });
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
- if @merge_request_diff.collected? - if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions' = render 'projects/merge_requests/show/versions'
= render "projects/diffs/diffs", diffs: @diffs = render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty? - elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
.alert.alert-warning
%h4
Changes view for this comparison is extremely large.
%p
You can
= link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink"
instead.
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note) = note_target_fields(@note)
= f.hidden_field :commit_id = f.hidden_field :commit_id
= f.hidden_field :line_code = f.hidden_field :line_code
......
- page_title "Services"
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
by entering by entering
%code /&lt;command_trigger_word&gt; help %code /&lt;command_trigger_word&gt; help
- unless enabled - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled - if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" - pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
- run_actions_text = "Perform common operations on this project: #{pretty_name}"
.well .well
This service allows GitLab users to perform common operations on this This service allows GitLab users to perform common operations on this
...@@ -9,85 +10,86 @@ ...@@ -9,85 +10,86 @@
%code /&lt;command&gt; help %code /&lt;command&gt; help
%br %br
%br %br
To setup this service: - unless @service.template?
%ul.list-unstyled To setup this service:
%li %ul.list-unstyled
1. %li
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' 1.
in your Slack team with these options: = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
in your Slack team with these options:
%hr %hr
.help-form .help-form
.form-group .form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label' = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block .col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team. %p Fill in the word that works best for your team.
%p %p
Suggestions: Suggestions:
%code= 'gitlab' %code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes %code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace %code= @project.path_with_namespace
.form-group .form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group .col-sm-10.col-xs-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn .input-group-btn
= clipboard_button(clipboard_target: '#url') = clipboard_button(clipboard_target: '#url')
.form-group .form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST .col-sm-10.col-xs-12.text-block POST
.form-group .form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group .col-sm-10.col-xs-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn .input-group-btn
= clipboard_button(clipboard_target: '#customize_name') = clipboard_button(clipboard_target: '#customize_name')
.form-group .form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block .col-sm-10.col-xs-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
.form-group .form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block Show this command in the autocomplete list .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
.form-group .form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group .col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn .input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description') = clipboard_button(clipboard_target: '#autocomplete_description')
.form-group .form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group .col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn .input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_usage_hint') = clipboard_button(clipboard_target: '#autocomplete_usage_hint')
.form-group .form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group .col-sm-10.col-xs-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn .input-group-btn
= clipboard_button(clipboard_target: '#descriptive_label') = clipboard_button(clipboard_target: '#descriptive_label')
%hr %hr
%ul.list-unstyled %ul.list-unstyled
%li %li
2. Paste the 2. Paste the
%strong Token %strong Token
into the field below into the field below
%li %li
3. Select the 3. Select the
%strong Active %strong Active
checkbox, press checkbox, press
%strong Save changes %strong Save changes
and start using GitLab inside Slack! and start using GitLab inside Slack!
- page_title 'Integrations'
= render 'projects/hooks/index'
= render 'projects/services/index'
- form = local_assigns.fetch(:f) - form = local_assigns.fetch(:f)
- commits = local_assigns[:commits]
- project = @target_project || @project - project = @target_project || @project
= form_errors(issuable) = form_errors(issuable)
...@@ -14,7 +15,7 @@ ...@@ -14,7 +15,7 @@
= form.label :title, class: 'control-label' = form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
= render 'shared/issuable/form/description', issuable: issuable, form: form = render 'shared/issuable/form/description', issuable: issuable, form: form
......
- issuable = local_assigns.fetch(:issuable) - issuable = local_assigns.fetch(:issuable)
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty? - no_issuable_templates = issuable_templates(issuable).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8' - div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
...@@ -18,6 +19,9 @@ ...@@ -18,6 +19,9 @@
%strong Work In Progress %strong Work In Progress
merge request to be merged when it's ready. merge request to be merged when it's ready.
.js-no-wip-explanation .js-no-wip-explanation
- if has_wip_commits
It looks like you have some WIP commits in this branch.
%br
%a.js-toggle-wip{ href: '', tabindex: -1 } %a.js-toggle-wip{ href: '', tabindex: -1 }
Start the title with Start the title with
%code WIP: %code WIP:
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
.issuable-detail .issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do = link_to [project.namespace.becomes(Namespace), project, issuable] do
%span.issuable-number >= issuable.to_reference %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label| - issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
= title = title
- if show_counter - if show_counter
.right .right
= issuables.size = number_with_delimiter(issuables.size)
.pull-right= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize - class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.pull-right.light #{milestone.percent_complete(current_user)}% complete .pull-right.light #{milestone.percent_complete(current_user)}% complete
.row .row
.col-sm-6 .col-sm-6
= link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot; &middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone) .col-sm-6= milestone_progress_bar(milestone)
......
- page_title "Webhooks"
- context_title = @project ? 'project' : 'group' - context_title = @project ? 'project' : 'group'
.row.prepend-top-default .row.prepend-top-default
......
class BuildQueueWorker
include Sidekiq::Worker
include BuildQueue
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
Ci::UpdateBuildQueueService.new.execute(build)
end
end
end
---
title: Reduce DB-load for build-queues by storing last_update in Redis
merge_request: 8084
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.
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