Commit 5fb89926 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee' into 'master'

CE upstream

Closes gitlab-ce#26762

See merge request !1075
parents 8129ba3f 94e33032
...@@ -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,9 +9,10 @@ ...@@ -9,9 +9,10 @@
.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;
...@@ -20,7 +21,6 @@ ...@@ -20,7 +21,6 @@
.help-block { .help-block {
margin-top: 0; margin-top: 0;
} }
}
.form-group { .form-group {
margin-bottom: 5px; margin-bottom: 5px;
...@@ -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 {
......
...@@ -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?
......
...@@ -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,9 +25,18 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -25,9 +25,18 @@ class Projects::CompareController < Projects::ApplicationController
end end
def create def create
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, redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to]) params[:from], params[:to])
end 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)
......
...@@ -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
...@@ -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
......
...@@ -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
...@@ -509,6 +510,10 @@ module Ci ...@@ -509,6 +510,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
......
...@@ -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
......
...@@ -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
...@@ -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
......
...@@ -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,9 +125,19 @@ class TodoService ...@@ -123,9 +125,19 @@ 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
# 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 end
# When create a note we should: # When create a note we should:
...@@ -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)
......
...@@ -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
......
- 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|
......
...@@ -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
.row.js-visibility-select
.col-md-9
%label.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light'
= link_to "(?)", help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields| = f.fields_for :project_feature do |feature_fields|
.form_group.prepend-top-20 %fieldset.features
.row .row
.col-md-9 .col-md-9.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light' = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block Push files to be stored in this project %span.help-block View and edit files in this project
.col-md-3.js-repo-access-level .col-md-3.js-repo-access-level
= project_feature_access_select(:repository_access_level) = project_feature_access_select(:repository_access_level)
.col-sm-12
.row .row
.col-md-9.project-feature-nested .col-md-9.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream %span.help-block Submit changes to be merged upstream
.col-md-3 .col-md-3
= project_feature_access_select(:merge_requests_access_level) = project_feature_access_select(:merge_requests_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 :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge %span.help-block Submit, test and deploy your changes before merge
.col-md-3 .col-md-3
= project_feature_access_select(:builds_access_level) = project_feature_access_select(:builds_access_level)
.row .row
.col-md-9 .col-md-9.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository %span.help-block Share code pastes with others out of Git repository
.col-md-3 .col-md-3
= project_feature_access_select(:snippets_access_level) = project_feature_access_select(:snippets_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 :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project %span.help-block Lightweight issue tracking system for this project
.col-md-3 .col-md-3
= project_feature_access_select(:issues_access_level) = project_feature_access_select(:issues_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 :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation %span.help-block Pages for project documentation
.col-md-3 .col-md-3
= project_feature_access_select(:wiki_access_level) = 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
......
...@@ -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,6 +10,7 @@ ...@@ -9,6 +10,7 @@
%code /&lt;command&gt; help %code /&lt;command&gt; help
%br %br
%br %br
- unless @service.template?
To setup this service: To setup this service:
%ul.list-unstyled %ul.list-unstyled
%li %li
......
- 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)
......
---
title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs
merge_request: 8056
author: twonegatives
---
title: Updated project visibility settings UX
merge_request: 7645
author:
---
title: Support slash comand `/merge` for merging merge requests.
merge_request: 7746
author: Jarka Kadlecova
---
title: Handle HTTP errors in environment list
merge_request:
author:
---
title: Add sorting pipeline for a commit
merge_request: 8319
author: Takuya Noguchi
---
title: Add margin to markdown math blocks
merge_request:
author:
---
title: Fixes builds dropdown making request when clicked to be closed
merge_request: 8545
author:
---
title: Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip
merge_request: 8593
author:
---
title: Adjust ProjectStatistic#repository_size with values saved as MB
merge_request: 8616
author:
---
title: "Correct User-agent placement in robots.txt"
merge_request: 8623
author: Eric Sabelhaus
---
title: 'Copy commit SHA to clipboard'
merge_request: 8547
---
title: Pass Gitaly resource path to gitlab-workhorse if Gitaly is enabled
merge_request: 8440
author:
---
title: Link external build badge to its target URL
merge_request: 8611
author:
---
title: Hide build artifacts keep button if operation is not allowed
merge_request: 8501
author:
---
title: Fix Compare page throws 500 error when any branch/reference is not selected
merge_request: 8492
author: Martin Cabrera
---
title: Add hover state to MR comment reply button
merge_request:
author:
---
title: Show 'too many changes' message for created merge requests when they are too large
merge_request:
author:
---
title: Autoresize markdown preview
merge_request: 8607
author: Didem Acet
---
title: Fixed merge request tabs dont move when opening collapsed sidebar
merge_request:
author:
---
title: Use cached values to compute total issues count in milestone index pages
merge_request: 8518
author:
---
title: Speed up dashboard milestone index by scoping IssuesFinder to user authorized
projects
merge_request: 8524
author:
---
title: Switch to sassc-rails for faster stylesheet compilation
merge_request: 8556
author: Richard Macklin
---
title: Mark MR as WIP when pushing WIP commits
merge_request: 8124
author: Jurre Stender @jurre
...@@ -106,6 +106,7 @@ module Gitlab ...@@ -106,6 +106,7 @@ module Gitlab
config.assets.precompile << "merge_request_widget/ci_bundle.js" config.assets.precompile << "merge_request_widget/ci_bundle.js"
config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "issuable/issuable_bundle.js"
config.assets.precompile << "merge_request_widget/ci_bundle.js" config.assets.precompile << "merge_request_widget/ci_bundle.js"
config.assets.precompile << "issuable/issuable_bundle.js"
config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
......
{
"ignored_warnings": [
{
"warning_type": "Cross-Site Request Forgery",
"warning_code": 7,
"fingerprint": "dc562678129557cdb8b187217da304044547a3605f05fe678093dcb4b4d8bbe4",
"message": "'protect_from_forgery' should be called in Oauth::GeoAuthController",
"file": "app/controllers/oauth/geo_auth_controller.rb",
"line": 1,
"link": "http://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
"code": null,
"render_path": null,
"location": {
"type": "controller",
"controller": "Oauth::GeoAuthController"
},
"user_input": null,
"confidence": "High",
"note": ""
}
],
"updated": "2017-01-20 02:06:54 +0000",
"brakeman_version": "3.4.1"
}
...@@ -514,6 +514,12 @@ Settings.rack_attack.git_basic_auth['maxretry'] ||= 10 ...@@ -514,6 +514,12 @@ Settings.rack_attack.git_basic_auth['maxretry'] ||= 10
Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
#
# Gitaly
#
Settings['gitaly'] ||= Settingslogic.new({})
Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
# #
# Testing settings # Testing settings
# #
......
...@@ -100,6 +100,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -100,6 +100,7 @@ constraints(ProjectUrlConstrainer.new) do
get :pipelines get :pipelines
get :merge_check get :merge_check
post :merge post :merge
get :merge_widget_refresh
post :cancel_merge_when_build_succeeds post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
get :ci_environments_status get :ci_environments_status
......
class AddEstimateToIssuablesCe < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
unless column_exists?(:issues, :time_estimate)
add_column :issues, :time_estimate, :integer
end
unless column_exists?(:merge_requests, :time_estimate)
add_column :merge_requests, :time_estimate, :integer
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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