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'
gem 'chronic', '~> 0.10.2'
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 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
......@@ -263,7 +263,7 @@ end
group :development do
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 'bullet', '~> 5.2.0', require: false
......
......@@ -88,7 +88,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.3.2)
brakeman (3.4.1)
browser (2.2.0)
builder (3.2.2)
bullet (5.2.0)
......@@ -691,17 +691,12 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
sassc (1.11.1)
bundler
ffi (~> 1.9.6)
sass (>= 3.3.0)
sassc-rails (1.3.0)
railties (>= 4.0.0)
sass
sassc (~> 1.9)
sprockets (> 2.11)
sprockets-rails
tilt
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
......@@ -876,7 +871,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0)
brakeman (~> 3.4.0)
browser (~> 2.2)
bullet (~> 5.2.0)
bundler-audit (~> 0.5.0)
......@@ -1009,7 +1004,7 @@ DEPENDENCIES
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
sanitize (~> 2.0)
sassc-rails (~> 1.3.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
......
/* eslint-disable no-param-reassign */
/* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
/* global Flash */
//= require vue
//= require vue-resource
......@@ -10,41 +11,6 @@
(() => {
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', {
props: {
store: {
......@@ -81,10 +47,6 @@
},
computed: {
filteredEnvironments() {
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
},
scope() {
return this.$options.getQueryParameter('scope');
},
......@@ -111,7 +73,7 @@
const scope = this.$options.getQueryParameter('scope');
if (scope) {
this.visibility = scope;
this.store.storeVisibility(scope);
}
this.isLoading = true;
......@@ -121,6 +83,10 @@
.then((json) => {
this.store.storeEnvironments(json);
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
......@@ -188,7 +154,7 @@
<div class="blank-state blank-state-no-icon"
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.
</h2>
<p class="blank-state-text">
......@@ -202,13 +168,13 @@
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create">
class="btn btn-create js-new-environment-button">
New Environment
</a>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
......@@ -221,7 +187,7 @@
</tr>
</thead>
<tbody>
<template v-for="model in filteredEnvironments"
<template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
......
......@@ -10,6 +10,8 @@
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
this.state.visibility = 'available';
this.state.filteredEnvironments = [];
return this;
},
......@@ -59,7 +61,7 @@
if (occurs.length) {
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 {
acc.push({
name: environment.environment_type,
......@@ -73,13 +75,70 @@
}
return acc;
}, []).sort(this.sortByName);
}, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
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.
*
......
......@@ -48,8 +48,9 @@
},
DefaultOptions: {
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)) {
this.setting.highlightFirst = false;
return items;
}
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
......
......@@ -10,9 +10,9 @@
* The container should be the table element.
*
* The stage icon clicked needs to have the following HTML structure:
* <div>
* <button class="dropdown js-builds-dropdown-button"></button>
* <div class="js-builds-dropdown-container"></div>
* <div class="dropdown">
* <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
* <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div>
*/
(() => {
......@@ -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() {
const dropdownButtonSelector = 'button.js-builds-dropdown-button';
$(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
.on('click', dropdownButtonSelector, this.getBuildsList);
$(this.container).on('shown.bs.dropdown', this.getBuildsList);
}
/**
......@@ -52,11 +50,14 @@
/**
* 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}
*/
getBuildsList(e) {
const button = e.currentTarget;
const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint;
return $.ajax({
......
......@@ -3,6 +3,7 @@
/* global GLForm */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
/*= require autosave */
/*= require autosize */
......@@ -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.
......@@ -429,6 +440,7 @@
*/
Notes.prototype.addNote = function(xhr, note, status) {
this.handleCreateChanges(note);
return this.renderNote(note);
};
......
......@@ -7,6 +7,7 @@
//
(function () {
var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
......@@ -104,10 +105,14 @@
if (!$form) {
return;
}
lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
// toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
......@@ -119,9 +124,15 @@
return;
}
lastTextareaPreviewed = null;
if (lastTextareaHeight) {
$form.find('textarea.markdown-area').height(lastTextareaHeight);
}
// toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
// toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
......
......@@ -15,11 +15,21 @@
return $('.save-project-loader').show();
};
})(this));
this.initVisibilitySelect();
this.toggleSettings();
this.toggleSettingsOnclick();
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() {
var self = this;
......
......@@ -57,7 +57,7 @@
return function(response) {
var error;
if (response.errorCode) {
error = new U2FError(response.errorCode);
error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
} else {
return _this.renderAuthenticated(JSON.stringify(response));
......
......@@ -5,21 +5,21 @@
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() {
function U2FError(errorCode) {
function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
this.u2fFlowType = u2fFlowType;
}
U2FError.prototype.message = function() {
switch (false) {
case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled):
return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details.";
case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE:
return "This device has already been registered with us.";
default:
return "There was a problem communicating with your device.";
if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
} else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
}
return "There was a problem communicating with your device.";
};
return U2FError;
......
......@@ -39,7 +39,7 @@
return function(response) {
var error;
if (response.errorCode) {
error = new U2FError(response.errorCode);
error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
} else {
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 @@
gl.VueStage = Vue.extend({
data() {
return {
count: 0,
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: ['stage', 'svgs', 'match'],
methods: {
fetchBuilds() {
if (this.count > 0) return null;
fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.count += 1;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
......@@ -39,7 +40,7 @@
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
svg() {
const icon = this.stage.status.icon;
const { icon } = this.stage.status;
const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)];
},
......@@ -50,18 +51,25 @@
template: `
<div>
<button
@click='fetchBuilds'
@click='fetchBuilds($event)'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
type="button">
type="button"
>
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<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>
</div>
`,
......
......@@ -324,7 +324,7 @@
&:focus {
cursor: text;
box-shadow: none;
border-color: $border-color;
border-color: lighten($dropdown-input-focus-border, 20%);
color: $gray-darkest;
background-color: $gray-light;
}
......
......@@ -76,7 +76,7 @@
.filter-dropdown {
max-height: 215px;
overflow-x: scroll;
overflow: auto;
}
.filter-dropdown-item {
......@@ -86,7 +86,7 @@
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis;
overflow-y: hidden;
overflow: hidden;
border-radius: 0;
.fa {
......
......@@ -236,9 +236,13 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
.merge-request-tabs-holder.affix {
&:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
}
&.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
}
}
&.with-overlay {
......
......@@ -10,7 +10,7 @@
max-width: 100%;
}
*:first-child {
*:first-child:not(.katex-display) {
margin-top: 0;
}
......
......@@ -515,7 +515,6 @@ ul.notes {
.line-resolve-all-container {
.btn-group {
margin-top: -1px;
margin-left: -4px;
}
......
......@@ -44,8 +44,8 @@
.pipeline-info,
.pipeline-commit,
.pipeline-actions,
.pipeline-stages {
.pipeline-stages,
.pipeline-actions {
width: 20%;
}
}
......@@ -185,6 +185,7 @@
.stage-cell {
font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
......@@ -202,8 +203,8 @@
position: relative;
margin-right: 6px;
.tooltip {
white-space: nowrap;
.tooltip-inner {
padding: 3px 4px;
}
&:not(:last-child) {
......@@ -348,6 +349,7 @@
padding: $gl-padding;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
overflow: auto;
.stage-column-list,
.builds-container > ul {
......
......@@ -9,17 +9,17 @@
.new_project,
.edit-project {
fieldset {
&.features {
.sharing-and-permissions {
.header {
padding-top: $gl-vert-padding;
}
.label-light {
margin-bottom: 0;
}
.label-light {
margin-bottom: 0;
}
.help-block {
margin-top: 0;
}
.help-block {
margin-top: 0;
}
.form-group {
......@@ -198,7 +198,7 @@
margin: 15px 5px 0 0;
input {
height: 28px;
height: 27px;
}
}
......@@ -938,10 +938,18 @@ a.allowed-to-push {
}
}
.project-feature-nested {
.project-feature {
padding-top: 10px;
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
&.nested {
@media (min-width: $screen-sm-min) {
padding-left: 90px;
}
}
}
.project-repo-select {
......
......@@ -5,7 +5,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
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,
notice: 'Application settings saved successfully'
else
......
......@@ -18,7 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
@users = [*@users, current_user]
@users = [current_user, *@users]
end
if params[:author_id].present?
......
......@@ -136,7 +136,7 @@ module LfsRequest
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
......
......@@ -19,11 +19,11 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def milestones
@milestones = GlobalMilestone.build_collection(@projects, params)
@milestones = DashboardMilestone.build_collection(@projects, params)
end
def milestone
@milestone = GlobalMilestone.build(@projects, params[:title])
@milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
end
......@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
@build ||= project.builds.find_by!(id: params[:id])
@build ||= project.builds.find_by!(id: params[:id]).present(user: current_user)
end
def build_path(build)
......
......@@ -12,7 +12,6 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit
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 :authorize_edit_tree!, only: [:revert, :cherry_pick]
......@@ -106,10 +105,6 @@ class Projects::CommitController < Projects::ApplicationController
}
end
def define_status_vars
@ci_pipelines = project.pipelines.where(sha: commit.sha)
end
def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
......
......@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController
end
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])
end
end
private
......
......@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
def index
@hooks = @project.hooks
@hook = ProjectHook.new
end
def create
@hook = @project.hooks.new(hook_params)
@hook.save
if @hook.valid?
redirect_to namespace_project_hooks_path(@project.namespace, @project)
else
unless @hook.valid?
@hooks = @project.hooks.select(&:persisted?)
render :index
flash[:alert] = @hook.errors.full_messages.join.html_safe
end
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def test
......@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
redirect_to namespace_project_hooks_path(@project.namespace, @project)
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
private
......
......@@ -373,6 +373,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
RebaseWorker.perform_async(@merge_request.id, current_user.id)
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
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
......
......@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end
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)
Banzai::NoteRenderer.render([@note], @project, current_user)
......
......@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def index
@services = @project.find_or_initialize_services
end
def edit
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
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
def render_overflow_warning?(diff_files)
diffs = @merge_request_diff.presence || diff_files
diffs.overflow?
end
end
......@@ -208,6 +208,10 @@ module GitlabRoutingHelper
end
# 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)
namespace_project_settings_members_path(project.namespace, project, *args)
end
......
......@@ -19,6 +19,14 @@ module MergeRequestsHelper
}
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)
classes = "merge-request"
classes << " closed" if mr.closed?
......
......@@ -436,4 +436,15 @@ module ProjectsHelper
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
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
......@@ -11,9 +11,10 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
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::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
end
end
......
......@@ -14,6 +14,55 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}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 :import_sources
serialize :disabled_oauth_sign_in_sources, Array
......@@ -176,49 +225,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
create(
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,
)
create(DEFAULTS)
end
def elasticsearch_host
......
......@@ -2,6 +2,7 @@ module Ci
class Build < CommitStatus
include TokenAuthenticatable
include AfterCommitQueue
include Presentable
prepend EE::Build
belongs_to :runner
......@@ -92,6 +93,12 @@ module Ci
end
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|
build.run_after_commit do
BuildHooksWorker.perform_async(id)
......@@ -509,6 +516,10 @@ module Ci
end
end
def has_expiring_artifacts?
artifacts_expire_at.present?
end
def keep_artifacts!
self.update(artifacts_expire_at: nil)
end
......
......@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
......@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do
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)
......@@ -122,8 +125,36 @@ module Ci
]
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
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
unless has_tags? || run_untagged?
errors.add(:tags_list,
......
......@@ -318,6 +318,20 @@ class Commit
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
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
def commit_reference(from_project, referable_commit_id, full: false)
......
......@@ -7,11 +7,14 @@ module Milestoneish
def total_items_count(user)
memoize_per_user(user, :total_items_count) do
issues_count = count_issues_by_state(user).values.sum
issues_count + merge_requests.size
total_issues_count(user) + merge_requests.size
end
end
def total_issues_count(user)
count_issues_by_state(user).values.sum
end
def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
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
before_validation :set_default_values
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
# GitHub compatible API
alias_attribute :context, :name
......@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus
def tags
[:external]
end
def detailed_status(current_user)
Gitlab::Ci::Status::External::Factory
.new(self, current_user)
.fabricate!
end
end
......@@ -96,6 +96,10 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
TodoService.new.merge_request_became_unmergeable(merge_request)
end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
......@@ -949,10 +953,22 @@ class MergeRequest < ActiveRecord::Base
end
def has_commits?
commits_count > 0
merge_request_diff && commits_count > 0
end
def has_no_commits?
!has_commits?
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
......@@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base
# and save it as array of hashes in st_diffs db field
def save_diffs
new_attributes = {}
new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
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
new_attributes[:real_size] = compare.diffs.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
new_attributes[:state] = :collected
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
new_attributes[:st_diffs] = new_diffs
update_columns_serialized(new_attributes)
end
......
......@@ -1541,8 +1541,10 @@ class Project < ActiveRecord::Base
actual_size_limit != 0
end
def changes_will_exceed_size_limit?(size_mb)
size_limit_enabled? && (size_mb > actual_size_limit || size_mb + repository_and_lfs_size > actual_size_limit)
def changes_will_exceed_size_limit?(size_in_bytes)
size_limit_enabled? &&
(size_in_bytes > actual_size_limit ||
size_in_bytes + repository_and_lfs_size > actual_size_limit)
end
def environments_for(ref, commit: nil, with_tags: false)
......
......@@ -29,8 +29,9 @@ class ProjectStatistics < ActiveRecord::Base
self.commit_count = project.repository.commit_count
end
# Repository#size needs to be converted from MB to Byte.
def update_repository_size
self.repository_size = project.repository.size
self.repository_size = project.repository.size * 1.megabyte
end
def update_lfs_objects_size
......
......@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3
MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature
UNMERGEABLE = 6
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked,
APPROVAL_REQUIRED => :approval_required
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable
}
belongs_to :author, class_name: "User"
......@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base
end
end
def unmergeable?
action == UNMERGEABLE
end
def build_failed?
action == BUILD_FAILED
end
......
......@@ -53,6 +53,10 @@ class BasePolicy
def self.class_for(subject)
return GlobalPolicy if subject.nil?
if subject.class.try(:presenter?)
subject = subject.subject
end
subject.class.ancestors.each do |klass|
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
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)
result = {
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
return @group
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)
@group.parent = nil
@group.errors.add(:parent_id, 'manage access required to create subgroup')
......
......@@ -12,6 +12,9 @@ module Groups
end
end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(group)
group.assign_attributes(params)
begin
......
......@@ -21,6 +21,7 @@ module MergeRequests
end
comment_mr_with_commits
mark_mr_as_wip_from_commits
execute_mr_web_hooks
reset_approvals_for_merge_requests
......@@ -151,6 +152,24 @@ module MergeRequests
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
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
......
......@@ -7,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
merge_from_slash_command(merge_request) if params[:merge]
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
......@@ -79,6 +81,19 @@ module MergeRequests
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
MergeRequests::ReopenService
end
......
module Notes
class CreateService < BaseService
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = project.notes.new(params)
note.author = current_user
note.system = false
......@@ -19,7 +21,8 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user)
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?
......
......@@ -19,10 +19,10 @@ module Notes
self.class.supported?(note, current_user)
end
def extract_commands(note)
def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note)
SlashCommands::InterpretService.new(project, current_user).
SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable)
end
......
......@@ -22,6 +22,9 @@ module Projects
return @project
end
# Repository size limit comes as MB from the view
assign_repository_size_limit_as_bytes(@project)
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
......
......@@ -13,6 +13,9 @@ module Projects
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_repository_storage = params.delete(:repository_storage)
......
......@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
attr_reader :issuable
attr_reader :issuable, :options
# 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.
......@@ -13,7 +13,8 @@ module SlashCommands
opts = {
issuable: issuable,
current_user: current_user,
project: project
project: project,
params: params
}
content, commands = extractor.extract_commands(content, opts)
......@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen'
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'
params '<New title>'
condition do
......
......@@ -208,6 +208,12 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
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)
body = "resolved all discussions"
......
......@@ -98,10 +98,12 @@ class TodoService
# 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)
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
# When new approvers are added for a merge request:
......@@ -123,11 +125,21 @@ class TodoService
# 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 merge_user as done
#
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.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
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
......@@ -254,10 +266,14 @@ class TodoService
create_todos(approvers.map(&:user), attributes)
end
def create_build_failed_todo(merge_request)
author = merge_request.author
attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
create_todos(author, attributes)
def create_build_failed_todo(merge_request, todo_author)
attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
create_todos(todo_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
def attributes_for_target(target)
......
......@@ -67,7 +67,7 @@
= f.label :repository_size_limit, class: 'control-label col-sm-2' do
Size limit per repository (MB)
.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
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")
......
......@@ -3,7 +3,7 @@
.todo-item.todo-block
.todo-title.title
- unless todo.build_failed?
- unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
%span.author-name
......
......@@ -3,6 +3,6 @@
= f.label :repository_size_limit, class: 'control-label' do
Repository size limit (MB)
.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
= size_limit_message_for_group(@group)
......@@ -8,14 +8,10 @@
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link(controller: :hooks) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
= nav_link(controller: :integrations) do
= link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
Webhooks
= nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
%span
Services
Integrations
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%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 @@
%p.build-detail-row
The artifacts were removed
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- elsif @build.artifacts_expire_at
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
%span.js-artifacts-remove= @build.artifacts_expire_at
- if @build.artifacts?
.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
Keep
......
......@@ -53,8 +53,10 @@
.prepend-top-default
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- if @build.erased_by_user?
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
#js-build-scroll.scroll-controls
.scroll-step
......
......@@ -13,7 +13,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.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
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
......
......@@ -7,4 +7,4 @@
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
%span.badge= @ci_pipelines.count
%span.badge= @commit.pipelines.size
......@@ -3,4 +3,4 @@
= render "commit_box"
= render "ci_menu"
= render "pipelines_list", pipelines: @ci_pipelines
= render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc)
......@@ -18,8 +18,8 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
- if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files
- if render_overflow_warning?(diff_files)
= render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file|
......
......@@ -38,7 +38,7 @@
.form-group
= f.label :repository_size_limit, class: 'label-light' do
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
= size_limit_message(@project)
......@@ -50,55 +50,63 @@
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
%fieldset.features.append-bottom-0
%fieldset.append-bottom-0
%h5.prepend-top-0
Feature Visibility
= f.fields_for :project_feature do |feature_fields|
.form_group.prepend-top-20
.row
.col-md-9
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block Push files to be stored in this project
.col-md-3.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.col-sm-12
.row
.col-md-9.project-feature-nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-3
= project_feature_access_select(:merge_requests_access_level)
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|
%fieldset.features
.row
.col-md-9.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block View and edit files in this project
.col-md-3.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.row
.col-md-9.project-feature-nested
= feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
.row
.col-md-9.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-3
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-9
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
= project_feature_access_select(:snippets_access_level)
.row
.col-md-9.project-feature.nested
= feature_fields.label :builds_access_level, "Builds", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
.row
.col-md-9
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
= project_feature_access_select(:issues_access_level)
.row
.col-md-9.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
= project_feature_access_select(:snippets_access_level)
.row
.col-md-9
= 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)
.row
.col-md-9.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
= 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?
.row
.col-md-9
......
......@@ -11,7 +11,7 @@
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%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|
= 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_branch
= f.hidden_field :target_project_id
......
......@@ -113,3 +113,6 @@
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/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.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 @@
= 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 :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note)
= f.hidden_field :commit_id
= f.hidden_field :line_code
......
- page_title "Services"
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
......
......@@ -8,8 +8,8 @@
by entering
%code /&lt;command_trigger_word&gt; help
- unless enabled
- unless enabled || @service.template?
= 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
- 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
This service allows GitLab users to perform common operations on this
......@@ -9,85 +10,86 @@
%code /&lt;command&gt; help
%br
%br
To setup this service:
%ul.list-unstyled
%li
1.
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
in your Slack team with these options:
- unless @service.template?
To setup this service:
%ul.list-unstyled
%li
1.
= 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
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#url')
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#url')
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#customize_name')
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#customize_name')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= 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')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= 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')
.form-group
= 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
.form-group
= 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
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
.form-group
= 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
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_usage_hint')
.form-group
= 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
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_usage_hint')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
.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'
.input-group-btn
= clipboard_button(clipboard_target: '#descriptive_label')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
.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'
.input-group-btn
= clipboard_button(clipboard_target: '#descriptive_label')
%hr
%hr
%ul.list-unstyled
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
%ul.list-unstyled
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
- page_title 'Integrations'
= render 'projects/hooks/index'
= render 'projects/services/index'
- form = local_assigns.fetch(:f)
- commits = local_assigns[:commits]
- project = @target_project || @project
= form_errors(issuable)
......@@ -14,7 +15,7 @@
= form.label :title, class: 'control-label'
= 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
......
- issuable = local_assigns.fetch(:issuable)
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
......@@ -18,6 +19,9 @@
%strong Work In Progress
merge request to be merged when it's ready.
.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 }
Start the title with
%code WIP:
......
......@@ -16,7 +16,7 @@
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
.issuable-detail
= 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|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
......
......@@ -8,8 +8,7 @@
= title
- if show_counter
.right
= issuables.size
.pull-right= number_with_delimiter(issuables.size)
= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
......
......@@ -9,7 +9,7 @@
.pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.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;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
......
- page_title "Webhooks"
- context_title = @project ? 'project' : 'group'
.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