Commit 0674fb62 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-06-02

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 4d675052 c7f644e4
...@@ -392,6 +392,15 @@ Style/OpMethod: ...@@ -392,6 +392,15 @@ Style/OpMethod:
Style/ParenthesesAroundCondition: Style/ParenthesesAroundCondition:
Enabled: true Enabled: true
# This cop (by default) checks for uses of methods Hash#has_key? and
# Hash#has_value? where it enforces Hash#key? and Hash#value?
# It is configurable to enforce the inverse, using `verbose` method
# names also.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: true
# Checks for an obsolete RuntimeException argument in raise/fail. # Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException: Style/RedundantException:
Enabled: true Enabled: true
......
...@@ -236,13 +236,6 @@ Style/PerlBackrefs: ...@@ -236,13 +236,6 @@ Style/PerlBackrefs:
Style/PredicateName: Style/PredicateName:
Enabled: false Enabled: false
# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: false
# Offense count: 65 # Offense count: 65
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
......
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props, no-new */
/* global GroupsSelect */
/* global ProjectSelect */
import UsersSelect from './users_select';
import './groups_select';
import './project_select';
class AuditLogs {
constructor() {
this.initFilters();
}
initFilters() {
new ProjectSelect();
new GroupsSelect();
new UsersSelect();
this.initFilterDropdown($('.js-type-filter'), 'event_type', null, () => {
$('.hidden-filter-value').val('');
$('form.filter-form').submit();
});
$('.project-item-select').on('click', () => {
$('form.filter-form').submit();
});
$('form.filter-form').on('submit', function applyFilters(event) {
event.preventDefault();
gl.utils.visitUrl(`${this.action}?${$(this).serialize()}`);
});
}
initFilterDropdown($dropdown, fieldName, searchFields, cb) {
const dropdownOptions = {
fieldName,
selectable: true,
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: () => $dropdown.closest('form.filter-form').submit(),
};
if (cb) {
dropdownOptions.clicked = cb;
}
$dropdown.glDropdown(dropdownOptions);
}
}
export default AuditLogs;
...@@ -61,6 +61,7 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -61,6 +61,7 @@ import ShortcutsBlob from './shortcuts_blob';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
import AuditLogs from './audit_logs';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -392,6 +393,9 @@ import ApproversSelect from './approvers_select'; ...@@ -392,6 +393,9 @@ import ApproversSelect from './approvers_select';
case 'admin:emails:show': case 'admin:emails:show':
new AdminEmailSelect(); new AdminEmailSelect();
break; break;
case 'admin:audit_logs:index':
new AuditLogs();
break;
case 'projects:repository:show': case 'projects:repository:show':
// Initialize Protected Branch Settings // Initialize Protected Branch Settings
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
......
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
if (job.import_status === 'finished') { if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success"); job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>'); return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') { } else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else { } else {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import Api from './api'; import Api from './api';
(function() { (function () {
this.ProjectSelect = (function() { this.ProjectSelect = (function () {
function ProjectSelect() { function ProjectSelect() {
$('.js-projects-dropdown-toggle').each(function(i, dropdown) { $('.js-projects-dropdown-toggle').each(function (i, dropdown) {
var $dropdown; var $dropdown;
$dropdown = $(dropdown); $dropdown = $(dropdown);
return $dropdown.glDropdown({ return $dropdown.glDropdown({
...@@ -13,16 +13,16 @@ import Api from './api'; ...@@ -13,16 +13,16 @@ import Api from './api';
search: { search: {
fields: ['name_with_namespace'] fields: ['name_with_namespace']
}, },
data: function(term, callback) { data: function (term, callback) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by'); var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) { finalCallback = function (projects) {
return callback(projects); return callback(projects);
}; };
if (this.includeGroups) { if (this.includeGroups) {
projectsCallback = function(projects) { projectsCallback = function (projects) {
var groupsCallback; var groupsCallback;
groupsCallback = function(groups) { groupsCallback = function (groups) {
var data; var data;
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
...@@ -35,24 +35,32 @@ import Api from './api'; ...@@ -35,24 +35,32 @@ import Api from './api';
if (this.groupId) { if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback); return Api.groupProjects(this.groupId, term, projectsCallback);
} else { } else {
return Api.projects(term, { order_by: orderBy }, projectsCallback); return Api.projects(term, {
order_by: orderBy
}, projectsCallback);
} }
}, },
url: function(project) { url: function (project) {
return project.web_url; return project.web_url;
}, },
text: function(project) { text: function (project) {
return project.name_with_namespace; return project.name_with_namespace;
} }
}); });
}); });
$('.ajax-project-select').each(function(i, select) { $('.ajax-project-select').each(function (i, select) {
var placeholder; var placeholder;
var idAttribute;
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups'); this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('allprojects') || false;
this.orderBy = $(select).data('order-by') || 'id'; this.orderBy = $(select).data('order-by') || 'id';
<<<<<<< HEAD
this.withIssuesEnabled = $(select).data('with-issues-enabled'); this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
=======
idAttribute = $(select).data('idattribute') || 'web_url';
>>>>>>> origin/master
placeholder = "Search for project"; placeholder = "Search for project";
if (this.includeGroups) { if (this.includeGroups) {
...@@ -61,10 +69,10 @@ import Api from './api'; ...@@ -61,10 +69,10 @@ import Api from './api';
return $(select).select2({ return $(select).select2({
placeholder: placeholder, placeholder: placeholder,
minimumInputLength: 0, minimumInputLength: 0,
query: (function(_this) { query: (function (_this) {
return function(query) { return function (query) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
finalCallback = function(projects) { finalCallback = function (projects) {
var data; var data;
data = { data = {
results: projects results: projects
...@@ -72,9 +80,9 @@ import Api from './api'; ...@@ -72,9 +80,9 @@ import Api from './api';
return query.callback(data); return query.callback(data);
}; };
if (_this.includeGroups) { if (_this.includeGroups) {
projectsCallback = function(projects) { projectsCallback = function (projects) {
var groupsCallback; var groupsCallback;
groupsCallback = function(groups) { groupsCallback = function (groups) {
var data; var data;
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
...@@ -89,16 +97,20 @@ import Api from './api'; ...@@ -89,16 +97,20 @@ import Api from './api';
} else { } else {
return Api.projects(query.term, { return Api.projects(query.term, {
order_by: _this.orderBy, order_by: _this.orderBy,
<<<<<<< HEAD
with_issues_enabled: _this.withIssuesEnabled, with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled with_merge_requests_enabled: _this.withMergeRequestsEnabled
=======
membership: !_this.allProjects
>>>>>>> origin/master
}, projectsCallback); }, projectsCallback);
} }
}; };
})(this), })(this),
id: function(project) { id: function (project) {
return project.web_url; return project[idAttribute];
}, },
text: function(project) { text: function (project) {
return project.name_with_namespace || project.name; return project.name_with_namespace || project.name;
}, },
dropdownCssClass: "ajax-project-dropdown" dropdownCssClass: "ajax-project-dropdown"
......
export const ACCESS_LEVELS = {
CREATE: 'create_access_levels',
};
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
};
export const ACCESS_LEVEL_NONE = 0;
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown'; import ProtectedTagDropdown from './protected_tag_dropdown';
...@@ -5,6 +8,12 @@ export default class ProtectedTagCreate { ...@@ -5,6 +8,12 @@ export default class ProtectedTagCreate {
constructor() { constructor() {
this.$form = $('.js-new-protected-tag'); this.$form = $('.js-new-protected-tag');
this.buildDropdowns(); this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
} }
buildDropdowns() { buildDropdowns() {
...@@ -14,15 +23,13 @@ export default class ProtectedTagCreate { ...@@ -14,15 +23,13 @@ export default class ProtectedTagCreate {
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown // Allowed to Create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown, $dropdown: $allowedToCreateDropdown,
data: gon.create_access_levels, accessLevelsData: gon.create_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
}); });
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown // Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({ this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'), $dropdown: this.$form.find('.js-protected-tag-select'),
...@@ -30,12 +37,60 @@ export default class ProtectedTagCreate { ...@@ -30,12 +37,60 @@ export default class ProtectedTagCreate {
}); });
} }
// This will run after clicked callback // Enable submit button after selecting an option
onSelect() { onSelect() {
// Enable submit button const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length);
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_tag: {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
}
});
formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length)); $.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData(),
})
.success(() => {
location.reload();
})
.fail(() => new Flash('Failed to protect the tag'));
} }
} }
...@@ -10,7 +10,7 @@ export default class ProtectedTagDropdown { ...@@ -10,7 +10,7 @@ export default class ProtectedTagDropdown {
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag'); this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag');
this.buildDropdown(); this.buildDropdown();
this.bindEvents(); this.bindEvents();
...@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown { ...@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown {
}; };
this.$dropdownContainer this.$dropdownContainer
.find('.create-new-protected-tag code') .find('.js-create-new-protected-tag code')
.text(tagName); .text(tagName);
} }
......
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit { export default class ProtectedTagEdit {
constructor(options) { constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap; this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.onSelectCallback = this.onSelect.bind(this);
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.buildDropdowns(); this.buildDropdowns();
} }
buildDropdowns() { buildDropdowns() {
// Allowed to create dropdown // Allowed to create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
$dropdown: this.$allowedToCreateDropdownButton, $dropdown: this.$allowedToCreateDropdownButton,
data: gon.create_access_levels, onSelect: this.onSelectOption.bind(this),
onSelect: this.onSelectCallback, onHide: this.onDropdownHide.bind(this),
}); });
} }
onSelect() { onSelectOption() {
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
// Do not update if one dropdown has not selected any option this.hasChanges = true;
if (!$allowedToCreateInput.length) return; this.updatePermissions();
}
this.$allowedToCreateDropdownButton.disable(); updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
/* eslint-disable no-param-reassign */
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
$.ajax({ return acc;
}, {});
return $.ajax({
type: 'POST', type: 'POST',
url: this.$wrap.data('url'), url: this.$wrap.data('url'),
dataType: 'json', dataType: 'json',
data: { data: {
_method: 'PATCH', _method: 'PATCH',
protected_tag: { protected_tag: formData,
create_access_levels_attributes: [{ },
id: this.$allowedToCreateDropdownButton.data('access-level-id'), success: (response) => {
access_level: $allowedToCreateInput.val(), this.hasChanges = false;
}],
}, Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
});
}, },
error() { error() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list')); $.scrollTo(0);
new Flash('Failed to update tag!');
}, },
}).always(() => { }).always(() => {
this.$allowedToCreateDropdownButton.enable(); this.$allowedToCreateDropdownButton.enable();
}); });
} }
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = _.findWhere(selectedItems, { user_id: currentItem.user_id });
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
} }
...@@ -72,6 +72,7 @@ export default { ...@@ -72,6 +72,7 @@ export default {
<span v-for="approver in approvedBy"> <span v-for="approver in approvedBy">
<link-to-member-avatar <link-to-member-avatar
:avatarSize="20" :avatarSize="20"
:avatar-url="approver.user.avatar_url"
extra-link-class="approver-avatar" extra-link-class="approver-avatar"
:display-name="approver.user.name" :display-name="approver.user.name"
:profile-url="approver.user.web_url" :profile-url="approver.user.web_url"
......
<script>
import successIcon from 'icons/_icon_status_success.svg';
import errorIcon from 'icons/_icon_status_failed.svg';
import issuesBlock from './mr_widget_code_quality_issues.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../lib/utils/text_utility';
export default {
name: 'MRWidgetCodeQuality',
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
issuesBlock,
loadingIcon,
},
data() {
return {
collapseText: 'Expand',
isCollapsed: true,
isLoading: false,
loadingFailed: false,
};
},
computed: {
stateIcon() {
return this.mr.codeclimateMetrics.newIssues.length ? errorIcon : successIcon;
},
hasNoneIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return !newIssues.length && !resolvedIssues.length;
},
hasIssues() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
return newIssues.length || resolvedIssues.length;
},
codeText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
let newIssuesText = '';
let resolvedIssuesText = '';
let text = '';
if (this.hasNoneIssues) {
text = 'No changes to code quality so far.';
} else if (this.hasIssues) {
if (newIssues.length) {
newIssuesText = `degraded on ${newIssues.length} ${this.pointsText(newIssues)}`;
}
if (resolvedIssues.length) {
resolvedIssuesText = `improved on ${resolvedIssues.length} ${this.pointsText(resolvedIssues)}`;
}
const connector = this.hasIssues ? 'and' : '';
text = `Code quality ${resolvedIssuesText} ${connector} ${newIssuesText}.`;
}
return text;
},
},
methods: {
pointsText(issues) {
return gl.text.pluralize('point', issues.length);
},
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? 'Expand' : 'Collapse';
this.collapseText = text;
},
handleError() {
this.isLoading = false;
this.loadingFailed = true;
},
},
created() {
const { head_path, base_path } = this.mr.codeclimate;
this.isLoading = true;
this.service.fetchCodeclimate(head_path)
.then(resp => resp.json())
.then((data) => {
this.mr.setCodeclimateHeadMetrics(data);
this.service.fetchCodeclimate(base_path)
.then(response => response.json())
.then(baseData => this.mr.setCodeclimateBaseMetrics(baseData))
.then(() => this.mr.compareCodeclimateMetrics())
.then(() => {
this.isLoading = false;
})
.catch(() => this.handleError());
})
.catch(() => this.handleError());
},
};
</script>
<template>
<section class="mr-widget-code-quality">
<div
v-if="isLoading"
class="padding-left">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Loading codeclimate report.
</div>
<div v-else-if="!isLoading && !loadingFailed">
<span
class="padding-left ci-status-icon"
:class="{
'ci-status-icon-failed': mr.codeclimateMetrics.newIssues.length,
'ci-status-icon-passed': mr.codeclimateMetrics.newIssues.length === 0
}"
v-html="stateIcon">
</span>
<span>
{{codeText}}
</span>
<button
type="button"
class="btn-link btn-blank"
v-if="hasIssues"
@click="toggleCollapsed">
{{collapseText}}
</button>
<div
class="code-quality-container"
v-if="hasIssues"
v-show="!isCollapsed">
<issues-block
class="js-mr-code-resolved-issues"
v-if="mr.codeclimateMetrics.resolvedIssues.length"
type="success"
:issues="mr.codeclimateMetrics.resolvedIssues"
/>
<issues-block
class="js-mr-code-new-issues"
v-if="mr.codeclimateMetrics.newIssues.length"
type="failed"
:issues="mr.codeclimateMetrics.newIssues"
/>
</div>
</div>
<div
v-else-if="loadingFailed"
class="padding-left">
Failed to load codeclimate report.
</div>
</section>
</template>
<script>
export default {
name: 'MRWidgetCodeQualityIssues',
props: {
issues: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
},
};
</script>
<template>
<ul class="mr-widget-code-quality-list">
<li
class="commit-sha"
:class="{
failed: type === 'failed',
success: type === 'success'
}
"v-for="issue in issues">
<i
class="fa"
:class="{
'fa-minus': type === 'failed',
'fa-plus': type === 'success'
}"
aria-hidden="true">
</i>
<span>
<span v-if="type === 'success'">Fixed:</span>
{{issue.check_name}}
{{issue.location.path}}
{{issue.location.positions}}
{{issue.location.lines}}
</span>
</li>
</ul>
</template>
...@@ -2,6 +2,7 @@ import CEWidgetOptions from '../mr_widget_options'; ...@@ -2,6 +2,7 @@ import CEWidgetOptions from '../mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals'; import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node'; import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import RebaseState from './components/states/mr_widget_rebase'; import RebaseState from './components/states/mr_widget_rebase';
import WidgetCodeQuality from './components/mr_widget_code_quality.vue';
export default { export default {
extends: CEWidgetOptions, extends: CEWidgetOptions,
...@@ -9,11 +10,16 @@ export default { ...@@ -9,11 +10,16 @@ export default {
'mr-widget-approvals': WidgetApprovals, 'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode, 'mr-widget-geo-secondary-node': GeoSecondaryNode,
'mr-widget-rebase': RebaseState, 'mr-widget-rebase': RebaseState,
'mr-widget-code-quality': WidgetCodeQuality,
}, },
computed: { computed: {
shouldRenderApprovals() { shouldRenderApprovals() {
return this.mr.approvalsRequired; return this.mr.approvalsRequired;
}, },
shouldRenderCodeQuality() {
const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path;
},
}, },
template: ` template: `
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
...@@ -29,6 +35,11 @@ export default { ...@@ -29,6 +35,11 @@ export default {
v-if="mr.approvalsRequired" v-if="mr.approvalsRequired"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-code-quality
v-if="shouldRenderCodeQuality"
:mr="mr"
:service="service"
/>
<component <component
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
......
...@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService { ...@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService {
rebase() { rebase() {
return this.rebaseResource.save(); return this.rebaseResource.save();
} }
fetchCodeclimate(endpoint) { // eslint-disable-line
return Vue.http.get(endpoint);
}
} }
import CEMergeRequestStore from '../../stores/mr_widget_store'; import CEMergeRequestStore from '../../stores/mr_widget_store';
export default class MergeRequestStore extends CEMergeRequestStore { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
this.initCodeclimate(data);
}
setData(data) { setData(data) {
this.initGeo(data); this.initGeo(data);
this.initSquashBeforeMerge(data); this.initSquashBeforeMerge(data);
...@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.isApproved = !this.approvalsLeft || false; this.isApproved = !this.approvalsLeft || false;
this.preventMerge = this.approvalsRequired && this.approvalsLeft; this.preventMerge = this.approvalsRequired && this.approvalsLeft;
} }
initCodeclimate(data) {
this.codeclimate = data.codeclimate;
this.codeclimateMetrics = {
headIssues: [],
baseIssues: [],
newIssues: [],
resolvedIssues: [],
};
}
setCodeclimateHeadMetrics(data) {
this.codeclimateMetrics.headIssues = data;
}
setCodeclimateBaseMetrics(data) {
this.codeclimateMetrics.baseIssues = data;
}
compareCodeclimateMetrics() {
const { headIssues, baseIssues } = this.codeclimateMetrics;
this.codeclimateMetrics.newIssues = this.filterByFingerprint(headIssues, baseIssues);
this.codeclimateMetrics.resolvedIssues = this.filterByFingerprint(baseIssues, headIssues);
}
filterByFingerprint(firstArray, secondArray) { // eslint-disable-line
return firstArray.filter(item => !secondArray.find(el => el.fingerprint === item.fingerprint));
}
} }
...@@ -88,9 +88,7 @@ export default { ...@@ -88,9 +88,7 @@ export default {
cb.call(null, res); cb.call(null, res);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
initPolling() { initPolling() {
this.pollingInterval = new gl.SmartInterval({ this.pollingInterval = new gl.SmartInterval({
...@@ -137,9 +135,7 @@ export default { ...@@ -137,9 +135,7 @@ export default {
document.body.appendChild(el); document.body.appendChild(el);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
......
...@@ -2,10 +2,12 @@ import Timeago from 'timeago.js'; ...@@ -2,10 +2,12 @@ import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
<<<<<<< HEAD
this.gitlabLogo = data.gitlabLogo; this.gitlabLogo = data.gitlabLogo;
=======
>>>>>>> origin/master
this.setData(data); this.setData(data);
} }
...@@ -137,5 +139,4 @@ export default class MergeRequestStore { ...@@ -137,5 +139,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(event.updated_at); return timeagoInstance.format(event.updated_at);
} }
} }
...@@ -871,3 +871,42 @@ ...@@ -871,3 +871,42 @@
} }
} }
} }
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.padding-left {
padding-left: $gl-padding;
}
.ci-status-icon {
vertical-align: sub;
svg {
width: 22px;
height: 22px;
margin-right: 4px;
}
}
.code-quality-container {
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
.mr-widget-code-quality-list {
list-style: none;
padding: 0 36px;
margin: 0;
li.success {
color: $green-500;
}
li.failed {
color: $red-500;
}
}
}
}
...@@ -683,18 +683,19 @@ pre.light-well { ...@@ -683,18 +683,19 @@ pre.light-well {
} }
} }
.new_protected_branch { a.allowed-to-merge,
a.allowed-to-push {
cursor: pointer;
}
.new-protected-branch,
.new-protected-tag {
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: normal; font-weight: normal;
} }
} }
a.allowed-to-merge,
a.allowed-to-push {
cursor: pointer;
}
.protected-branch-push-access-list, .protected-branch-push-access-list,
.protected-branch-merge-access-list { .protected-branch-merge-access-list {
a { a {
...@@ -702,7 +703,8 @@ a.allowed-to-push { ...@@ -702,7 +703,8 @@ a.allowed-to-push {
} }
} }
.create-new-protected-branch-button { .create-new-protected-branch-button,
.create-new-protected-tag-button {
@include dropdown-link; @include dropdown-link;
width: 100%; width: 100%;
......
...@@ -172,10 +172,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -172,10 +172,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:elasticsearch_search, :elasticsearch_search,
:repository_size_limit, :repository_size_limit,
:shared_runners_minutes, :shared_runners_minutes,
:minimum_mirror_sync_time,
:geo_status_timeout, :geo_status_timeout,
:elasticsearch_experimental_indexer, :elasticsearch_experimental_indexer,
:check_namespace_plan :check_namespace_plan,
:mirror_max_delay,
:mirror_max_capacity,
:mirror_capacity_threshold
] ]
end end
end end
class Admin::AuditLogsController < Admin::ApplicationController
def index
@events = LogFinder.new(audit_logs_params).execute
@entity = case audit_logs_params[:event_type]
when 'User'
User.find_by_id(audit_logs_params[:user_id])
when 'Project'
Project.find_by_id(audit_logs_params[:project_id])
when 'Group'
Namespace.find_by_id(audit_logs_params[:group_id])
else
nil
end
end
def audit_logs_params
params.permit(:page, :event_type, :user_id, :project_id, :group_id)
end
end
...@@ -12,14 +12,7 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -12,14 +12,7 @@ class Projects::ImportsController < Projects::ApplicationController
def create def create
if @project.update_attributes(import_params) if @project.update_attributes(import_params)
@project.reload @project.reload.import_schedule
if @project.import_failed?
@project.import_retry
else
@project.import_start
@project.add_import_job
end
end end
redirect_to namespace_project_import_path(@project.namespace, @project) redirect_to namespace_project_import_path(@project.namespace, @project)
......
...@@ -14,10 +14,10 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -14,10 +14,10 @@ class Projects::MirrorsController < Projects::ApplicationController
def update def update
if @project.update_attributes(mirror_params) if @project.update_attributes(mirror_params)
if @project.mirror? if @project.mirror?
@project.update_mirror @project.force_import_job!
flash[:notice] = "Mirroring settings were successfully updated. The project is being updated." flash[:notice] = "Mirroring settings were successfully updated. The project is being updated."
elsif @project.mirror_changed? elsif project.previous_changes.has_key?('mirror')
flash[:notice] = "Mirroring was successfully disabled." flash[:notice] = "Mirroring was successfully disabled."
else else
flash[:notice] = "Mirroring settings were successfully updated." flash[:notice] = "Mirroring settings were successfully updated."
...@@ -34,9 +34,10 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -34,9 +34,10 @@ class Projects::MirrorsController < Projects::ApplicationController
@project.update_remote_mirrors @project.update_remote_mirrors
flash[:notice] = "The remote repository is being updated..." flash[:notice] = "The remote repository is being updated..."
else else
@project.update_mirror @project.force_import_job!
flash[:notice] = "The repository is being updated..." flash[:notice] = "The repository is being updated..."
end end
redirect_to_repository_settings(@project) redirect_to_repository_settings(@project)
end end
...@@ -48,7 +49,6 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -48,7 +49,6 @@ class Projects::MirrorsController < Projects::ApplicationController
def mirror_params def mirror_params
params.require(:project).permit(:mirror, :import_url, :mirror_user_id, params.require(:project).permit(:mirror, :import_url, :mirror_user_id,
:mirror_trigger_builds, :sync_time, :mirror_trigger_builds, remote_mirrors_attributes: [:url, :id, :enabled])
remote_mirrors_attributes: [:url, :id, :enabled])
end end
end end
...@@ -23,7 +23,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController ...@@ -23,7 +23,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def protected_ref_params def protected_ref_params
params.require(:protected_branch).permit(:name, params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id, :user_id, :_destroy, :group_id], merge_access_levels_attributes: access_level_attributes,
push_access_levels_attributes: [:access_level, :id, :user_id, :_destroy, :group_id]) push_access_levels_attributes: access_level_attributes)
end end
end end
...@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController ...@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
format.js { head :ok } format.js { head :ok }
end end
end end
protected
def access_level_attributes
%i(access_level id user_id _destroy group_id)
end
end end
class Projects::ProtectedTags::ApplicationController < Projects::ApplicationController
protected
def load_protected_tag
@protected_tag = @project.protected_tags.find(params[:protected_tag_id])
end
end
module Projects
module ProtectedTags
class CreateAccessLevelsController < ProtectedTags::ApplicationController
before_action :load_protected_tag, only: [:destroy]
def destroy
@create_access_level = @protected_tag.create_access_levels.find(params[:id])
@create_access_level.destroy
redirect_to namespace_project_protected_tag_path(@project.namespace, @project, @protected_tag),
notice: "Successfully deleted. #{@create_access_level.humanize} will not be able to create this protected tag."
end
end
end
end
...@@ -22,6 +22,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController ...@@ -22,6 +22,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
end end
def protected_ref_params def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id]) params.require(:protected_tag).permit(:name, create_access_levels_attributes: access_level_attributes)
end end
end end
...@@ -31,6 +31,7 @@ module Projects ...@@ -31,6 +31,7 @@ module Projects
{ {
selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level }, selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level }, selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
selected_create_access_levels: @protected_tag.create_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel), create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel), push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel) merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
......
class LogFinder
PER_PAGE = 25
ENTITY_COLUMN_TYPES = {
'User' => :user_id,
'Group' => :group_id,
'Project' => :project_id
}.freeze
def initialize(params)
@params = params
end
def execute
AuditEvent.order(id: :desc).where(conditions).page(@params[:page]).per(PER_PAGE)
end
private
def conditions
return nil unless entity_column
{ entity_type: @params[:event_type] }.tap do |hash|
hash[:entity_id] = @params[entity_column] if entity_present?
end
end
def entity_column
@entity_column ||= ENTITY_COLUMN_TYPES[@params[:event_type]]
end
def entity_present?
@params[entity_column] && @params[entity_column] != '0'
end
end
...@@ -39,7 +39,7 @@ class TodosFinder ...@@ -39,7 +39,7 @@ class TodosFinder
private private
def action_id? def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) action_id.present? && Todo::ACTION_NAMES.key?(action_id.to_i)
end end
def action_id def action_id
......
module AuditLogsHelper
def event_type_options
[
{ id: '', text: 'All Events' },
{ id: 'Group', text: 'Group Events' },
{ id: 'Project', text: 'Project Events' },
{ id: 'User', text: 'User Events' }
]
end
def admin_user_dropdown_label(default_label)
if @entity
@entity.name
else
default_label
end
end
def admin_project_dropdown_label(default_label)
if @entity
@entity.name_with_namespace
else
default_label
end
end
def admin_namespace_dropdown_label(default_label)
if @entity
@entity.full_path
else
default_label
end
end
end
...@@ -24,6 +24,15 @@ module BranchesHelper ...@@ -24,6 +24,15 @@ module BranchesHelper
ProtectedBranch.protected?(project, branch.name) ProtectedBranch.protected?(project, branch.name)
end end
# Returns a hash were keys are types of access levels (user, role), and
# values are the number of access levels of the particular type.
def access_level_frequencies(access_levels)
access_levels.reduce(Hash.new(0)) do |frequencies, access_level|
frequencies[access_level.type] += 1
frequencies
end
end
def access_levels_data(access_levels) def access_levels_data(access_levels)
access_levels.map do |level| access_levels.map do |level|
if level.type == :user if level.type == :user
......
module DropdownsHelper module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block) def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown #{options[:wrapper_class] if options.has_key?(:wrapper_class)}" do content_tag :div, class: "dropdown #{options[:wrapper_class] if options.key?(:wrapper_class)}" do
data_attr = { toggle: "dropdown" } data_attr = { toggle: "dropdown" }
if options.has_key?(:data) if options.key?(:data)
data_attr = options[:data].merge(data_attr) data_attr = options[:data].merge(data_attr)
end end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options) dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
if options.has_key?(:toggle_link) if options.key?(:toggle_link)
dropdown_output = dropdown_toggle_link(toggle_text, data_attr, options) dropdown_output = dropdown_toggle_link(toggle_text, data_attr, options)
end end
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do
output = "" output = ""
if options.has_key?(:title) if options.key?(:title)
output << dropdown_title(options[:title]) output << dropdown_title(options[:title])
end end
if options.has_key?(:filter) if options.key?(:filter)
output << dropdown_filter(options[:placeholder]) output << dropdown_filter(options[:placeholder])
end end
output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.has_key?(:content_class)}") do output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do
capture(&block) if block && !options.has_key?(:footer_content) capture(&block) if block && !options.key?(:footer_content)
end end
if block && options[:footer_content] if block && options[:footer_content]
...@@ -45,7 +45,7 @@ module DropdownsHelper ...@@ -45,7 +45,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {}) def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label] default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down') output << icon('chevron-down')
output.html_safe output.html_safe
...@@ -53,7 +53,7 @@ module DropdownsHelper ...@@ -53,7 +53,7 @@ module DropdownsHelper
end end
def dropdown_toggle_link(toggle_text, data_attr, options = {}) def dropdown_toggle_link(toggle_text, data_attr, options = {})
output = content_tag(:a, toggle_text, class: "dropdown-toggle-text #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), data: data_attr) output = content_tag(:a, toggle_text, class: "dropdown-toggle-text #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), data: data_attr)
output.html_safe output.html_safe
end end
......
...@@ -4,10 +4,4 @@ module MirrorHelper ...@@ -4,10 +4,4 @@ module MirrorHelper
message << "<br>To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above." if can?(current_user, :push_code, @project) message << "<br>To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above." if can?(current_user, :push_code, @project)
message message
end end
def mirror_sync_time_options
Gitlab::Mirror::SYNC_TIME_OPTIONS.select do |key, value|
value >= current_application_settings.minimum_mirror_sync_time
end
end
end end
...@@ -432,6 +432,12 @@ module ProjectsHelper ...@@ -432,6 +432,12 @@ module ProjectsHelper
end end
end end
def can_force_update_mirror?(project)
return true unless project.mirror_last_update_at
Time.now - project.mirror_last_update_at >= 5.minutes
end
def membership_locked? def membership_locked?
if @project.group && @project.group.membership_lock if @project.group && @project.group.membership_lock
true true
......
...@@ -154,13 +154,9 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -154,13 +154,9 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :minimum_mirror_sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level| value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level) unless Gitlab::VisibilityLevel.options.value?(level)
record.errors.add(attr, "'#{level}' is not a valid visibility level") record.errors.add(attr, "'#{level}' is not a valid visibility level")
end end
end end
...@@ -168,7 +164,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -168,7 +164,7 @@ class ApplicationSetting < ActiveRecord::Base
validates_each :import_sources do |record, attr, value| validates_each :import_sources do |record, attr, value|
value&.each do |source| value&.each do |source|
unless Gitlab::ImportSources.options.has_value?(source) unless Gitlab::ImportSources.options.value?(source)
record.errors.add(attr, "'#{source}' is not a import source") record.errors.add(attr, "'#{source}' is not a import source")
end end
end end
...@@ -186,8 +182,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -186,8 +182,6 @@ class ApplicationSetting < ActiveRecord::Base
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
after_update :update_mirror_cron_job, if: :minimum_mirror_sync_time_changed?
after_commit do after_commit do
Rails.cache.write(CACHE_KEY, self) Rails.cache.write(CACHE_KEY, self)
end end
...@@ -218,7 +212,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -218,7 +212,7 @@ class ApplicationSetting < ActiveRecord::Base
ApplicationSetting.define_attribute_methods ApplicationSetting.define_attribute_methods
end end
def self.defaults_ce def self.defaults
{ {
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
...@@ -269,20 +263,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -269,20 +263,6 @@ class ApplicationSetting < ActiveRecord::Base
} }
end end
def self.defaults_ee
{
elasticsearch_url: ENV['ELASTIC_URL'] || 'http://localhost:9200',
elasticsearch_aws: false,
elasticsearch_aws_region: ENV['ELASTIC_REGION'] || 'us-east-1',
minimum_mirror_sync_time: Gitlab::Mirror::FIFTEEN,
repository_size_limit: 0
}
end
def self.defaults
defaults_ce.merge(defaults_ee)
end
def self.create_from_defaults def self.create_from_defaults
create(defaults) create(defaults)
end end
...@@ -295,13 +275,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -295,13 +275,6 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
def update_mirror_cron_job
Project.mirror.where('sync_time < ?', minimum_mirror_sync_time)
.update_all(sync_time: minimum_mirror_sync_time)
Gitlab::Mirror.configure_cron_job!
end
def elasticsearch_url def elasticsearch_url
read_attribute(:elasticsearch_url).split(',').map(&:strip) read_attribute(:elasticsearch_url).split(',').map(&:strip)
end end
......
...@@ -9,11 +9,15 @@ class AuditEvent < ActiveRecord::Base ...@@ -9,11 +9,15 @@ class AuditEvent < ActiveRecord::Base
after_initialize :initialize_details after_initialize :initialize_details
def author_name
details[:author_name].blank? ? user&.name : details[:author_name]
end
def initialize_details def initialize_details
self.details = {} if details.nil? self.details = {} if details.nil?
end end
def author_name def present
self.user.try(:name) || details[:author_name] AuditEventPresenter.new(self)
end end
end end
...@@ -35,6 +35,7 @@ module Ci ...@@ -35,6 +35,7 @@ module Ci
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual).relevant } scope :manual_actions, ->() { where(when: :manual).relevant }
scope :codeclimate, ->() { where(name: 'codeclimate') }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -48,8 +49,8 @@ module Ci ...@@ -48,8 +49,8 @@ module Ci
before_destroy { unscoped_project } before_destroy { unscoped_project }
after_create :execute_hooks after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed? after_commit :update_project_statistics_after_save, on: [:create, :update]
after_destroy :update_project_statistics after_commit :update_project_statistics, on: :destroy
class << self class << self
# This is needed for url_for to work, # This is needed for url_for to work,
...@@ -414,6 +415,11 @@ module Ci ...@@ -414,6 +415,11 @@ module Ci
trace trace
end end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
private private
def update_artifacts_size def update_artifacts_size
...@@ -492,5 +498,11 @@ module Ci ...@@ -492,5 +498,11 @@ module Ci
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end end
def update_project_statistics_after_save
if previous_changes.include?('artifacts_size')
update_project_statistics
end
end
end end
end end
...@@ -403,6 +403,10 @@ module Ci ...@@ -403,6 +403,10 @@ module Ci
.fabricate! .fabricate!
end end
def codeclimate_artifact
artifacts.codeclimate.find(&:has_codeclimate_json?)
end
private private
def pipeline_data def pipeline_data
......
...@@ -177,7 +177,7 @@ class Commit ...@@ -177,7 +177,7 @@ class Commit
if RequestStore.active? if RequestStore.active?
key = "commit_author:#{author_email.downcase}" key = "commit_author:#{author_email.downcase}"
# nil is a valid value since no author may exist in the system # nil is a valid value since no author may exist in the system
if RequestStore.store.has_key?(key) if RequestStore.store.key?(key)
@author = RequestStore.store[key] @author = RequestStore.store[key]
else else
@author = find_author_by_any_email @author = find_author_by_any_email
......
...@@ -18,7 +18,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -18,7 +18,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true validates :name, presence: true
alias_attribute :author, :user alias_attribute :author, :user
scope :failed_but_allowed, -> do scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled]) where(allow_failure: true, status: [:failed, :canceled])
end end
...@@ -83,14 +83,15 @@ class CommitStatus < ActiveRecord::Base ...@@ -83,14 +83,15 @@ class CommitStatus < ActiveRecord::Base
next if transition.loopback? next if transition.loopback?
commit_status.run_after_commit do commit_status.run_after_commit do
pipeline.try do |pipeline| if pipeline
if complete? || manual? if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id) PipelineProcessWorker.perform_async(pipeline.id)
else else
PipelineUpdateWorker.perform_async(pipeline.id) PipelineUpdateWorker.perform_async(pipeline.id)
end end
ExpireJobCacheWorker.perform_async(commit_status.id)
end end
ExpireJobCacheWorker.perform_async(commit_status.id)
end end
end end
......
...@@ -36,20 +36,16 @@ module Approvable ...@@ -36,20 +36,16 @@ module Approvable
# #
def number_of_potential_approvers def number_of_potential_approvers
has_access = ['access_level > ?', Member::REPORTER] has_access = ['access_level > ?', Member::REPORTER]
all_approvers = all_approvers_including_groups
wheres = [ wheres = [
"id IN (#{project.members.where(has_access).select(:user_id).to_sql})" "id IN (#{project.project_authorizations.where(has_access).select(:user_id).to_sql})"
] ]
all_approvers = all_approvers_including_groups
if all_approvers.any? if all_approvers.any?
wheres << "id IN (#{all_approvers.map(&:id).join(', ')})" wheres << "id IN (#{all_approvers.map(&:id).join(', ')})"
end end
if project.group
wheres << "id IN (#{project.group.members.where(has_access).select(:user_id).to_sql})"
end
users = User users = User
.active .active
.where("(#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql})") .where("(#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql})")
...@@ -73,7 +69,6 @@ module Approvable ...@@ -73,7 +69,6 @@ module Approvable
# #
def overall_approvers def overall_approvers
approvers_relation = approvers_overwritten? ? approvers : target_project.approvers approvers_relation = approvers_overwritten? ? approvers : target_project.approvers
approvers_relation = approvers_relation.where.not(user_id: author.id) if author approvers_relation = approvers_relation.where.not(user_id: author.id) if author
approvers_relation approvers_relation
...@@ -113,7 +108,7 @@ module Approvable ...@@ -113,7 +108,7 @@ module Approvable
end end
def approvers_overwritten? def approvers_overwritten?
approvers.any? || approver_groups.any? approvers.to_a.any? || approver_groups.to_a.any?
end end
def can_approve?(user) def can_approve?(user)
......
...@@ -3,7 +3,7 @@ module ProtectedBranchAccess ...@@ -3,7 +3,7 @@ module ProtectedBranchAccess
included do included do
include ProtectedRefAccess include ProtectedRefAccess
include EE::ProtectedBranchAccess include EE::ProtectedRefAccess
belongs_to :protected_branch belongs_to :protected_branch
......
...@@ -8,32 +8,50 @@ module ProtectedRef ...@@ -8,32 +8,50 @@ module ProtectedRef
validates :project, presence: true validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher delegate :matching, :matches?, :wildcard?, to: :ref_matcher
end
def commit
project.commit(self.name)
end
class_methods do
def protected_ref_access_levels(*types)
types.each do |type|
has_many :"#{type}_access_levels", dependent: :destroy
validates :"#{type}_access_levels", length: { minimum: 0 }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
def self.protected_ref_accessible_to?(ref, user, action:) # Returns access levels that grant the specified access type to the given user / group.
access_level_class = const_get("#{type}_access_level".classify)
protected_type = self.model_name.singular
scope :"#{type}_access_by_user", -> (user) { access_level_class.joins(protected_type.to_sym).where("#{protected_type}_id" => self.ids).merge(access_level_class.by_user(user)) }
scope :"#{type}_access_by_group", -> (group) { access_level_class.joins(protected_type.to_sym).where("#{protected_type}_id" => self.ids).merge(access_level_class.by_group(group)) }
end
end
def protected_ref_accessible_to?(ref, user, action:)
access_levels_for_ref(ref, action: action).any? do |access_level| access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.check_access(user) access_level.check_access(user)
end end
end end
def self.developers_can?(action, ref) def developers_can?(action, ref)
access_levels_for_ref(ref, action: action).any? do |access_level| access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER access_level.access_level == Gitlab::Access::DEVELOPER
end end
end end
def self.access_levels_for_ref(ref, action:) def access_levels_for_ref(ref, action:)
self.matching(ref).map(&:"#{action}_access_levels").flatten self.matching(ref).map(&:"#{action}_access_levels").flatten
end end
def self.matching(ref_name, protected_refs: nil) def matching(ref_name, protected_refs: nil)
ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
end end
end end
def commit
project.commit(self.name)
end
private private
def ref_matcher def ref_matcher
......
...@@ -3,6 +3,7 @@ module ProtectedTagAccess ...@@ -3,6 +3,7 @@ module ProtectedTagAccess
included do included do
include ProtectedRefAccess include ProtectedRefAccess
include EE::ProtectedRefAccess
belongs_to :protected_tag belongs_to :protected_tag
......
...@@ -7,12 +7,54 @@ module EE ...@@ -7,12 +7,54 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
include IgnorableColumn
ignore_column :minimum_mirror_sync_time
validates :shared_runners_minutes, validates :shared_runners_minutes,
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :mirror_max_delay,
presence: true,
numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
validates :mirror_max_capacity,
presence: true,
numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
validates :mirror_capacity_threshold,
presence: true,
numericality: { allow_nil: true, only_integer: true, greater_than: 0 }
validate :mirror_capacity_threshold_less_than
end
module ClassMethods
def defaults
super.merge(
elasticsearch_url: ENV['ELASTIC_URL'] || 'http://localhost:9200',
elasticsearch_aws: false,
elasticsearch_aws_region: ENV['ELASTIC_REGION'] || 'us-east-1',
repository_size_limit: 0,
mirror_max_delay: Settings.gitlab['mirror_max_delay'],
mirror_max_capacity: Settings.gitlab['mirror_max_capacity'],
mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold']
)
end
end end
def should_check_namespace_plan? def should_check_namespace_plan?
check_namespace_plan? && (::Gitlab.com? || Rails.env.development?) check_namespace_plan? && (::Gitlab.com? || Rails.env.development?)
end end
private
def mirror_capacity_threshold_less_than
return unless mirror_max_capacity && mirror_capacity_threshold
if mirror_capacity_threshold > mirror_max_capacity
errors.add(:mirror_capacity_threshold, "Project's mirror capacity threshold can't be higher than it's maximum capacity")
end
end
end end
end end
...@@ -7,8 +7,28 @@ module EE ...@@ -7,8 +7,28 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
include IgnorableColumn
ignore_column :sync_time
after_save :create_mirror_data, if: ->(project) { project.mirror? && project.mirror_changed? }
after_save :destroy_mirror_data, if: ->(project) { !project.mirror? && project.mirror_changed? }
has_one :mirror_data, dependent: :delete, autosave: true, class_name: 'ProjectMirrorData'
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only } scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirrors_to_sync, -> do
mirror.joins(:mirror_data).where("next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')", Time.now).
order_by(:next_execution_timestamp).limit(::Gitlab::Mirror.available_capacity)
end
scope :stuck_mirrors, -> do
mirror.joins(:mirror_data).
where("(import_status = 'started' AND project_mirror_data.last_update_started_at < :limit) OR (import_status = 'scheduled' AND project_mirror_data.last_update_scheduled_at < :limit)",
{ limit: 20.minutes.ago })
end
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :statistics, allow_nil: true to: :statistics, allow_nil: true
...@@ -44,6 +64,19 @@ module EE ...@@ -44,6 +64,19 @@ module EE
config.address&.gsub(wildcard, full_path) config.address&.gsub(wildcard, full_path)
end end
def force_import_job!
self.mirror_data.set_next_execution_to_now!
UpdateAllMirrorsWorker.perform_async
end
def add_import_job
if import? && !repository_exists?
super
elsif mirror?
RepositoryUpdateMirrorWorker.perform_async(self.id)
end
end
private private
def licensed_feature_available?(feature) def licensed_feature_available?(feature)
...@@ -57,6 +90,10 @@ module EE ...@@ -57,6 +90,10 @@ module EE
end end
end end
def destroy_mirror_data
mirror_data.destroy
end
def service_desk_available? def service_desk_available?
return @service_desk_available if defined?(@service_desk_available) return @service_desk_available if defined?(@service_desk_available)
......
# EE-specific code related to protected branch access levels. # EE-specific code related to protected branch/tag access levels.
# #
# Note: Don't directly include this concern into a model class. # Note: Don't directly include this concern into a model class.
# Instead, include `ProtectedBranchAccess`, which in turn includes # Instead, include `ProtectedBranchAccess` or `ProtectedTagAccess`, which in
# this concern. A number of methods here depend on `ProtectedBranchAccess` # turn include this concern. A number of methods here depend on
# being next up in the ancestor chain. # `ProtectedRefAccess` being next up in the ancestor chain.
module EE module EE
module ProtectedBranchAccess module ProtectedRefAccess
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
belongs_to :user belongs_to :user
belongs_to :group belongs_to :group
validates :group_id, uniqueness: { scope: :protected_branch, allow_nil: true } protected_type = self.parent.model_name.singular
validates :user_id, uniqueness: { scope: :protected_branch, allow_nil: true } validates :group_id, uniqueness: { scope: protected_type, allow_nil: true }
validates :access_level, uniqueness: { scope: :protected_branch, if: :role?, validates :user_id, uniqueness: { scope: protected_type, allow_nil: true }
validates :access_level, uniqueness: { scope: protected_type, if: :role?,
conditions: -> { where(user_id: nil, group_id: nil) } } conditions: -> { where(user_id: nil, group_id: nil) } }
scope :by_user, -> (user) { where(user: user ) } scope :by_user, -> (user) { where(user: user ) }
......
...@@ -15,6 +15,9 @@ module EE ...@@ -15,6 +15,9 @@ module EE
# column directly. # column directly.
validate :auditor_requires_license_add_on, if: :auditor validate :auditor_requires_license_add_on, if: :auditor
validate :cannot_be_admin_and_auditor validate :cannot_be_admin_and_auditor
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
to: :namespace
end end
module ClassMethods module ClassMethods
......
...@@ -95,6 +95,18 @@ class GeoNode < ActiveRecord::Base ...@@ -95,6 +95,18 @@ class GeoNode < ActiveRecord::Base
self.primary? ? false : !oauth_application.present? self.primary? ? false : !oauth_application.present?
end end
def update_clone_url!
update_clone_url
# Update with update_column to prevent calling callbacks as this method will
# be called in an initializer and we don't want other callbacks
# to mess with uninitialized dependencies.
if clone_url_prefix_changed?
Rails.logger.info "Geo: modified clone_url_prefix to #{clone_url_prefix}"
update_column(:clone_url_prefix, clone_url_prefix)
end
end
private private
def geo_api_url(suffix) def geo_api_url(suffix)
...@@ -134,6 +146,7 @@ class GeoNode < ActiveRecord::Base ...@@ -134,6 +146,7 @@ class GeoNode < ActiveRecord::Base
if self.primary? if self.primary?
self.oauth_application = nil self.oauth_application = nil
update_clone_url
else else
update_oauth_application! update_oauth_application!
update_system_hook! update_system_hook!
...@@ -149,6 +162,10 @@ class GeoNode < ActiveRecord::Base ...@@ -149,6 +162,10 @@ class GeoNode < ActiveRecord::Base
end end
end end
def update_clone_url
self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix
end
def update_oauth_application! def update_oauth_application!
self.build_oauth_application if oauth_application.nil? self.build_oauth_application if oauth_application.nil?
self.oauth_application.name = "Geo node: #{self.url}" self.oauth_application.name = "Geo node: #{self.url}"
......
...@@ -271,9 +271,9 @@ class Issue < ActiveRecord::Base ...@@ -271,9 +271,9 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user] json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
if options.has_key?(:labels) if options.key?(:labels)
json[:labels] = labels.as_json( json[:labels] = labels.as_json(
project: project, project: project,
only: [:id, :title, :description, :color, :priority], only: [:id, :title, :description, :color, :priority],
......
...@@ -5,8 +5,7 @@ class IssueAssignee < ActiveRecord::Base ...@@ -5,8 +5,7 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :assignee, class_name: "User", foreign_key: :user_id belongs_to :assignee, class_name: "User", foreign_key: :user_id
# EE-specific # EE-specific
after_create :update_elasticsearch_index after_commit :update_elasticsearch_index, on: [:create, :destroy]
after_destroy :update_elasticsearch_index
# EE-specific # EE-specific
def update_elasticsearch_index def update_elasticsearch_index
......
require 'digest/md5' require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i LAST_USED_AT_REFRESH_TIME = 1.day.to_i
...@@ -27,10 +26,10 @@ class Key < ActiveRecord::Base ...@@ -27,10 +26,10 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true delegate :name, :email, to: :user, prefix: true
after_create :add_to_shell after_commit :add_to_shell, on: :create
after_create :notify_user after_commit :notify_user, on: :create
after_create :post_create_hook after_create :post_create_hook
after_destroy :remove_from_shell after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
def key=(value) def key=(value)
...@@ -95,6 +94,6 @@ class Key < ActiveRecord::Base ...@@ -95,6 +94,6 @@ class Key < ActiveRecord::Base
end end
def notify_user def notify_user
run_after_commit { NotificationService.new.new_key(self) } NotificationService.new.new_key(self)
end end
end end
...@@ -172,7 +172,7 @@ class Label < ActiveRecord::Base ...@@ -172,7 +172,7 @@ class Label < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:priority] = priority(options[:project]) if options.has_key?(:project) json[:priority] = priority(options[:project]) if options.key?(:project)
end end
end end
......
...@@ -6,8 +6,7 @@ class LfsObjectsProject < ActiveRecord::Base ...@@ -6,8 +6,7 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true validates :project_id, presence: true
after_create :update_project_statistics after_commit :update_project_statistics, on: [:create, :destroy]
after_destroy :update_project_statistics
private private
......
...@@ -28,7 +28,7 @@ class List < ActiveRecord::Base ...@@ -28,7 +28,7 @@ class List < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
if options.has_key?(:label) if options.key?(:label)
json[:label] = label.as_json( json[:label] = label.as_json(
project: board.project, project: board.project,
only: [:id, :title, :description, :color] only: [:id, :title, :description, :color]
......
...@@ -34,6 +34,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -34,6 +34,9 @@ class MergeRequest < ActiveRecord::Base
delegate :commits, :real_size, :commits_sha, :commits_count, delegate :commits, :real_size, :commits_sha, :commits_count,
to: :merge_request_diff, prefix: nil to: :merge_request_diff, prefix: nil
delegate :codeclimate_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :codeclimate_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
# When this attribute is true some MR validation is ignored # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
attr_accessor :allow_broken attr_accessor :allow_broken
...@@ -967,4 +970,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -967,4 +970,13 @@ class MergeRequest < ActiveRecord::Base
true true
end end
def base_pipeline
@base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha)
end
def has_codeclimate_data?
!!(head_codeclimate_artifact&.success? &&
base_codeclimate_artifact&.success?)
end
end end
...@@ -7,6 +7,7 @@ class Namespace < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class Namespace < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Routable include Routable
include AfterCommitQueue
# Prevent users from creating unreasonably deep level of nesting. # Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of # The number 20 was taken based on maximum nesting level of
...@@ -247,7 +248,9 @@ class Namespace < ActiveRecord::Base ...@@ -247,7 +248,9 @@ class Namespace < ActiveRecord::Base
# Remove namespace directroy async with delay so # Remove namespace directroy async with delay so
# GitLab has time to remove all projects first # GitLab has time to remove all projects first
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) run_after_commit do
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end
end end
end end
......
...@@ -170,7 +170,7 @@ class Project < ActiveRecord::Base ...@@ -170,7 +170,7 @@ class Project < ActiveRecord::Base
has_many :audit_events, as: :entity, dependent: :destroy has_many :audit_events, as: :entity, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :delete, class_name: 'ProjectImportData'
has_one :project_feature, dependent: :destroy has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy has_many :container_repositories, dependent: :destroy
...@@ -239,10 +239,6 @@ class Project < ActiveRecord::Base ...@@ -239,10 +239,6 @@ class Project < ActiveRecord::Base
validates :repository_size_limit, validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
validates :sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
with_options if: :mirror? do |project| with_options if: :mirror? do |project|
project.validates :import_url, presence: true project.validates :import_url, presence: true
project.validates :mirror_user, presence: true project.validates :mirror_user, presence: true
...@@ -273,7 +269,6 @@ class Project < ActiveRecord::Base ...@@ -273,7 +269,6 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct } scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) } scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
...@@ -327,8 +322,16 @@ class Project < ActiveRecord::Base ...@@ -327,8 +322,16 @@ class Project < ActiveRecord::Base
scope :excluding_project, ->(project) { where.not(id: project) } scope :excluding_project, ->(project) { where.not(id: project) }
state_machine :import_status, initial: :none do state_machine :import_status, initial: :none do
event :import_schedule do
transition [:none, :finished, :failed] => :scheduled
end
event :force_import_start do
transition [:none, :finished, :failed] => :started
end
event :import_start do event :import_start do
transition [:none, :finished] => :started transition scheduled: :started
end end
event :import_finish do event :import_finish do
...@@ -336,24 +339,57 @@ class Project < ActiveRecord::Base ...@@ -336,24 +339,57 @@ class Project < ActiveRecord::Base
end end
event :import_fail do event :import_fail do
transition started: :failed transition [:scheduled, :started] => :failed
end
event :import_retry do
transition failed: :started
end end
state :scheduled
state :started state :started
state :finished state :finished
state :failed state :failed
after_transition any => :finished, do: :reset_cache_and_import_attrs before_transition [:none, :finished, :failed] => :scheduled do |project, _|
project.mirror_data&.last_update_scheduled_at = Time.now
end
before_transition started: :finished do |project, transaction| after_transition [:none, :finished, :failed] => :scheduled do |project, _|
project.run_after_commit { add_import_job }
end
before_transition scheduled: :started do |project, _|
project.mirror_data&.last_update_started_at = Time.now
end
before_transition scheduled: :failed do |project, _|
if project.mirror? if project.mirror?
timestamp = DateTime.now timestamp = Time.now
project.mirror_last_update_at = timestamp
project.mirror_data.next_execution_timestamp = timestamp
end
end
after_transition [:scheduled, :started] => [:finished, :failed] do |project, _|
Gitlab::Mirror.decrement_capacity(project.id) if project.mirror?
end
before_transition started: :failed do |project, _|
if project.mirror?
project.mirror_last_update_at = Time.now
mirror_data = project.mirror_data
mirror_data.increment_retry_count!
mirror_data.set_next_execution_timestamp!
end
end
before_transition started: :finished do |project, _|
if project.mirror?
timestamp = Time.now
project.mirror_last_update_at = timestamp project.mirror_last_update_at = timestamp
project.mirror_last_successful_update_at = timestamp project.mirror_last_successful_update_at = timestamp
mirror_data = project.mirror_data
mirror_data.reset_retry_count!
mirror_data.set_next_execution_timestamp!
end end
if current_application_settings.elasticsearch_indexing? if current_application_settings.elasticsearch_indexing?
...@@ -361,8 +397,10 @@ class Project < ActiveRecord::Base ...@@ -361,8 +397,10 @@ class Project < ActiveRecord::Base
end end
end end
before_transition started: :failed do |project, transaction| after_transition started: :finished, do: :reset_cache_and_import_attrs
project.mirror_last_update_at = DateTime.now if project.mirror?
after_transition [:finished, :failed] => [:scheduled, :started] do |project, _|
Gitlab::Mirror.increment_capacity(project.id) if project.mirror?
end end
end end
...@@ -516,7 +554,9 @@ class Project < ActiveRecord::Base ...@@ -516,7 +554,9 @@ class Project < ActiveRecord::Base
end end
def reset_cache_and_import_attrs def reset_cache_and_import_attrs
ProjectCacheWorker.perform_async(self.id) run_after_commit do
ProjectCacheWorker.perform_async(self.id)
end
self.import_data&.destroy unless mirror? self.import_data&.destroy unless mirror?
end end
...@@ -575,9 +615,17 @@ class Project < ActiveRecord::Base ...@@ -575,9 +615,17 @@ class Project < ActiveRecord::Base
end end
def import_in_progress? def import_in_progress?
import_started? || import_scheduled?
end
def import_started?
import? && import_status == 'started' import? && import_status == 'started'
end end
def import_scheduled?
import_status == 'scheduled'
end
def import_failed? def import_failed?
import_status == 'failed' import_status == 'failed'
end end
...@@ -595,7 +643,10 @@ class Project < ActiveRecord::Base ...@@ -595,7 +643,10 @@ class Project < ActiveRecord::Base
end end
def updating_mirror? def updating_mirror?
mirror? && import_in_progress? && !empty_repo? return false unless mirror? && !empty_repo?
return true if import_in_progress?
self.mirror_data.next_execution_timestamp < Time.now
end end
def mirror_last_update_status def mirror_last_update_status
...@@ -620,20 +671,6 @@ class Project < ActiveRecord::Base ...@@ -620,20 +671,6 @@ class Project < ActiveRecord::Base
mirror_updated? && self.mirror_last_successful_update_at mirror_updated? && self.mirror_last_successful_update_at
end end
def update_mirror
return unless mirror? && repository_exists?
return if import_in_progress?
if import_failed?
import_retry
else
import_start
end
RepositoryUpdateMirrorWorker.perform_async(self.id)
end
def has_remote_mirror? def has_remote_mirror?
remote_mirrors.enabled.exists? remote_mirrors.enabled.exists?
end end
......
class ProjectMirrorData < ActiveRecord::Base
include Gitlab::CurrentSettings
BACKOFF_PERIOD = 24.seconds
JITTER = 6.seconds
belongs_to :project
validates :project, presence: true
validates :next_execution_timestamp, presence: true
before_validation on: :create do
self.next_execution_timestamp = Time.now
end
def reset_retry_count!
self.retry_count = 0
end
def increment_retry_count!
self.retry_count += 1
end
# We schedule the next sync time based on the duration of the
# last mirroring period and add it a fixed backoff period with a random jitter
def set_next_execution_timestamp!
timestamp = Time.now
retry_factor = [1, self.retry_count].max
delay = [base_delay(timestamp) * retry_factor, Gitlab::Mirror.max_delay].min
self.next_execution_timestamp = timestamp + delay
end
def set_next_execution_to_now!
self.update_attributes(next_execution_timestamp: Time.now)
end
private
def base_delay(timestamp)
duration = timestamp - self.last_update_started_at
(BACKOFF_PERIOD + rand(JITTER)) * duration.seconds
end
end
...@@ -2,48 +2,7 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -2,48 +2,7 @@ class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
has_many :merge_access_levels, dependent: :destroy protected_ref_access_levels :merge, :push
has_many :push_access_levels, dependent: :destroy
validates :merge_access_levels, length: { minimum: 0 }
validates :push_access_levels, length: { minimum: 0 }
accepts_nested_attributes_for :push_access_levels, allow_destroy: true
accepts_nested_attributes_for :merge_access_levels, allow_destroy: true
# Returns all merge access levels (for protected branches in scope) that grant merge
# access to the given user.
scope :merge_access_by_user, -> (user) { MergeAccessLevel.joins(:protected_branch).where(protected_branch_id: self.ids).merge(MergeAccessLevel.by_user(user)) }
# Returns all push access levels (for protected branches in scope) that grant push
# access to the given user.
scope :push_access_by_user, -> (user) { PushAccessLevel.joins(:protected_branch).where(protected_branch_id: self.ids).merge(PushAccessLevel.by_user(user)) }
# Returns all merge access levels (for protected branches in scope) that grant merge
# access to the given group.
scope :merge_access_by_group, -> (group) { MergeAccessLevel.joins(:protected_branch).where(protected_branch_id: self.ids).merge(MergeAccessLevel.by_group(group)) }
# Returns all push access levels (for protected branches in scope) that grant push
# access to the given group.
scope :push_access_by_group, -> (group) { PushAccessLevel.joins(:protected_branch).where(protected_branch_id: self.ids).merge(PushAccessLevel.by_group(group)) }
# Returns a hash were keys are types of push access levels (user, role), and
# values are the number of access levels of the particular type.
def push_access_level_frequencies
push_access_levels.reduce(Hash.new(0)) do |frequencies, access_level|
frequencies[access_level.type] = frequencies[access_level.type] + 1
frequencies
end
end
# Returns a hash were keys are types of merge access levels (user, role), and
# values are the number of access levels of the particular type.
def merge_access_level_frequencies
merge_access_levels.reduce(Hash.new(0)) do |frequencies, access_level|
frequencies[access_level.type] = frequencies[access_level.type] + 1
frequencies
end
end
# Check if branch name is marked as protected in the system # Check if branch name is marked as protected in the system
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
......
...@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base ...@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
has_many :create_access_levels, dependent: :destroy protected_ref_access_levels :create
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :create_access_levels
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present? self.matching(ref_name, protected_refs: project.protected_tags).present?
......
...@@ -68,7 +68,7 @@ class User < ActiveRecord::Base ...@@ -68,7 +68,7 @@ class User < ActiveRecord::Base
# #
# Namespace for personal projects # Namespace for personal projects
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true
# Profile # Profile
has_many :keys, -> do has_many :keys, -> do
......
class
AuditEventPresenter < Gitlab::View::Presenter::Simple
presents :audit_event
def author_name
audit_event.author_name || '(removed)'
end
def target
audit_event.details[:target_details]
end
def ip_address
audit_event.details[:ip_address]
end
def object
audit_event.details[:entity_path]
end
def date
audit_event.created_at.to_s(:db)
end
def action
Audit::Details.humanize(audit_event.details)
end
end
...@@ -197,6 +197,23 @@ class MergeRequestEntity < IssuableEntity ...@@ -197,6 +197,23 @@ class MergeRequestEntity < IssuableEntity
merge_request) merge_request)
end end
# EE-specific
expose :codeclimate, if: -> (mr, _) { mr.has_codeclimate_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_codeclimate_artifact) } do |merge_request|
raw_namespace_project_build_artifacts_url(merge_request.source_project.namespace,
merge_request.source_project,
merge_request.head_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_namespace_project_build_artifacts_url(merge_request.target_project.namespace,
merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: 'codeclimate.json')
end
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request
......
...@@ -29,7 +29,7 @@ class AuditEventService ...@@ -29,7 +29,7 @@ class AuditEventService
target_type: "User", target_type: "User",
target_details: user_name target_details: user_name
} }
when :update when :update, :override
{ {
change: "access_level", change: "access_level",
from: old_access_level, from: old_access_level,
...@@ -87,7 +87,8 @@ class AuditEventService ...@@ -87,7 +87,8 @@ class AuditEventService
author_id: @author.id, author_id: @author.id,
entity_id: @entity.id, entity_id: @entity.id,
entity_type: @entity.class.name, entity_type: @entity.class.name,
details: @details details: @details.merge(ip_address: @author.current_sign_in_ip,
entity_path: @entity.full_path)
) )
end end
end end
...@@ -93,7 +93,7 @@ module Geo ...@@ -93,7 +93,7 @@ module Geo
end end
def primary_ssh_path_prefix def primary_ssh_path_prefix
Gitlab::Geo.primary_ssh_path_prefix Gitlab::Geo.primary_node.clone_url_prefix
end end
def ssh_url_to_repo def ssh_url_to_repo
......
...@@ -148,7 +148,7 @@ class IssuableBaseService < BaseService ...@@ -148,7 +148,7 @@ class IssuableBaseService < BaseService
execute(params[:description], issuable) execute(params[:description], issuable)
# Avoid a description already set on an issuable to be overwritten by a nil # Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if params.has_key?(:description) params[:description] = description if params.key?(:description)
params.merge!(command_params) params.merge!(command_params)
end end
......
...@@ -51,15 +51,14 @@ module Projects ...@@ -51,15 +51,14 @@ module Projects
save_project_and_import_data(import_data) save_project_and_import_data(import_data)
@project.import_start if @project.import?
after_create_actions if @project.persisted? after_create_actions if @project.persisted?
if @project.errors.empty? if @project.errors.empty?
@project.add_import_job if @project.import? @project.import_schedule if @project.import?
else else
fail(error: @project.errors.full_messages.join(', ')) fail(error: @project.errors.full_messages.join(', '))
end end
@project @project
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
......
...@@ -7,11 +7,9 @@ module Projects ...@@ -7,11 +7,9 @@ module Projects
DELETED_FLAG = '+deleted'.freeze DELETED_FLAG = '+deleted'.freeze
def async_execute def async_execute
project.transaction do project.update_attribute(:pending_delete, true)
project.update_attribute(:pending_delete, true) job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}")
Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}")
end
end end
def execute def execute
...@@ -95,7 +93,11 @@ module Projects ...@@ -95,7 +93,11 @@ module Projects
if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path) if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path)
log_info("Repository \"#{path}\" moved to \"#{new_path}\"") log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
GitlabShellWorker.perform_in(5.minutes, :remove_repository, project.repository_storage_path, new_path)
project.run_after_commit do
# self is now project
GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage_path, new_path)
end
else else
false false
end end
......
...@@ -92,48 +92,77 @@ module SlashCommands ...@@ -92,48 +92,77 @@ module SlashCommands
desc 'Assign' desc 'Assign'
explanation do |users| explanation do |users|
"Assigns #{users.map(&:to_reference).to_sentence}." if users.any? users = issuable.is_a?(Issue) ? users : users.take(1)
"Assigns #{users.map(&:to_reference).to_sentence}."
end
params do
issuable.is_a?(Issue) ? '@user1 @user2' : '@user'
end end
params '@user'
condition do condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
parse_params do |assignee_param| parse_params do |assignee_param|
users = extract_references(assignee_param, :user) extract_users(assignee_param)
if users.empty?
users = User.where(username: assignee_param.split(' ').map(&:strip))
end
users
end end
command :assign do |users| command :assign do |users|
next if users.empty? next if users.empty?
if issuable.is_a?(Issue) if issuable.is_a?(Issue)
@updates[:assignee_ids] = users.map(&:id) # EE specific. In CE we should replace one assignee with another
@updates[:assignee_ids] = issuable.assignees.pluck(:id) + users.map(&:id)
else else
@updates[:assignee_id] = users.last.id @updates[:assignee_id] = users.last.id
end end
end end
desc 'Remove assignee' desc do
if issuable.is_a?(Issue)
'Remove all or specific assignee(s)'
else
'Remove assignee'
end
end
explanation do explanation do
"Removes assignee #{issuable.assignees.first.to_reference}." "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}"
end
params do
issuable.is_a?(Issue) ? '@user1 @user2' : ''
end end
condition do condition do
issuable.persisted? && issuable.persisted? &&
issuable.assignees.any? && issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end end
command :unassign do command :unassign do |unassign_param = nil|
users = extract_users(unassign_param)
if issuable.is_a?(Issue) if issuable.is_a?(Issue)
@updates[:assignee_ids] = [] @updates[:assignee_ids] =
if users.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
else else
@updates[:assignee_id] = nil @updates[:assignee_id] = nil
end end
end end
desc 'Change assignee(s)'
explanation do
'Change assignee(s)'
end
params '@user1 @user2'
condition do
issuable.is_a?(Issue) &&
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :reassign do |unassign_param|
@updates[:assignee_ids] = extract_users(unassign_param).map(&:id)
end
desc 'Set milestone' desc 'Set milestone'
explanation do |milestone| explanation do |milestone|
"Sets the milestone to #{milestone.to_reference}." if milestone "Sets the milestone to #{milestone.to_reference}." if milestone
...@@ -487,6 +516,18 @@ module SlashCommands ...@@ -487,6 +516,18 @@ module SlashCommands
end end
end end
def extract_users(params)
return [] if params.nil?
users = extract_references(params, :user)
if users.empty?
users = User.where(username: params.split(' ').map(&:strip))
end
users
end
def find_labels(labels_param) def find_labels(labels_param)
extract_references(labels_param, :label) | extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
......
...@@ -26,6 +26,12 @@ module Users ...@@ -26,6 +26,12 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end end
Project.includes(group: :owners).where(mirror_user: user).find_each do |project|
if project.group.present?
project.update(mirror_user: project.group.owners.first)
end
end
MigrateToGhostUserService.new(user).execute unless options[:hard_delete] MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
......
...@@ -71,10 +71,6 @@ ...@@ -71,10 +71,6 @@
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited. Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings") = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
.form-group
= f.label :minimum_mirror_sync_time, class: 'control-label col-sm-2'
.col-sm-10
= f.select :minimum_mirror_sync_time, options_for_select(Gitlab::Mirror::SYNC_TIME_OPTIONS, @application_setting.minimum_mirror_sync_time), {}, class: 'form-control'
.form-group .form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -105,6 +101,8 @@ ...@@ -105,6 +101,8 @@
Enabling this will only make licensed EE features available to projects if the project namespace's plan Enabling this will only make licensed EE features available to projects if the project namespace's plan
includes the feature or if the project is public. includes the feature or if the project is public.
= render partial: 'repository_mirrors_form', locals: { f: f }
%fieldset %fieldset
%legend Sign-up Restrictions %legend Sign-up Restrictions
.form-group .form-group
......
%fieldset
%legend Repository mirror settings
.form-group
= f.label :mirror_max_delay, class: 'control-label col-sm-2' do
Maximum delay (Hours)
.col-sm-10
= f.number_field :mirror_max_delay, class: 'form-control', min: 0
%span.help-block#mirror_max_delay_help_block
Maximum time between updates that a mirror can have when scheduled to synchronize.
.form-group
= f.label :mirror_max_capacity, class: 'control-label col-sm-2' do
Maximum capacity
.col-sm-10
= f.number_field :mirror_max_capacity, class: 'form-control', min: 0
%span.help-block#mirror_max_capacity_help_block
Maximum number of mirrors that can be synchronizing at the same time.
.form-group
= f.label :mirror_capacity_threshold, class: 'control-label col-sm-2' do
Capacity threshold
.col-sm-10
= f.number_field :mirror_capacity_threshold, class: 'form-control', min: 0
%span.help-block#mirror_capacity_threshold
Minimum capacity to be available before we schedule more mirrors preemptively.
- @no_container = true
- page_title 'Audit Log'
= render 'admin/background_jobs/head'
%div{ class: container_class }
.todos-filters
.row-content-block.second-block
= form_tag admin_audit_logs_path, method: :get, class: 'filter-form' do
.filter-item.inline
- if params[:event_type].present?
= hidden_field_tag(:event_type, params[:event_type])
- event_type = params[:event_type].presence || 'All'
= dropdown_tag("#{event_type} Events", options: { toggle_class: 'js-type-search js-filter-submit js-type-filter', dropdown_class: 'dropdown-menu-type dropdown-menu-selectable dropdown-menu-action js-filter-submit',
placeholder: 'Search types', data: { field_name: 'event_type', data: event_type_options, default_label: 'All Events' } })
- if params[:event_type] == 'User'
.filter-item.inline
- if params[:user_id].present?
= hidden_field_tag(:user_id, params[:user_id], class:'hidden-filter-value')
= dropdown_tag(admin_user_dropdown_label('User'), options: { toggle_class: 'js-user-search js-filter-submit', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable',
placeholder: 'Search users', data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, field_name: 'user_id' } })
- elsif params[:event_type] == 'Project'
.filter-item.inline
= project_select_tag(:project_id, { class: 'project-item-select hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: admin_project_dropdown_label('Search projects'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', allprojects: 'true'} })
- elsif params[:event_type] == 'Group'
.filter-item.inline
= groups_select_tag(:group_id, { required: true, class: 'group-item-select project-item-select hidden-filter-value', toggle_class: 'js-group-search js-group-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
placeholder: admin_namespace_dropdown_label('Search groups'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_available: true} })
- if @events.present?
%table.table
%thead
%tr
%th Author
%th Object
%th Action
%th Target
%th IP Address
%th Date
%tbody
- @events.map(&:present).each do |event|
%tr
%td= event.author_name
%td= event.object
%td= event.action
%td= event.target
%td= event.ip_address
%td= event.date
= paginate @events, theme: 'gitlab'
...@@ -23,3 +23,7 @@ ...@@ -23,3 +23,7 @@
= link_to admin_requests_profiles_path, title: 'Requests Profiles' do = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
%span %span
Requests Profiles Requests Profiles
= nav_link path: 'audit_logs#index' do
= link_to admin_audit_logs_path, title: 'Audit Log' do
%span
Audit Log
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span %span
Overview Overview
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles audit_logs)) do
= link_to admin_system_info_path, title: 'Monitoring' do = link_to admin_system_info_path, title: 'Monitoring' do
%span %span
Monitoring Monitoring
......
...@@ -43,9 +43,6 @@ ...@@ -43,9 +43,6 @@
They need to have at least master access to this project. They need to have at least master access to this project.
- if @project.builds_enabled? - if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f = render "shared/mirror_trigger_builds_setting", f: f
.form-group
= f.label :sync_time, "Synchronization time", class: "label-light append-bottom-0"
= f.select :sync_time, options_for_select(mirror_sync_time_options, @project.sync_time), {}, class: 'form-control project-mirror-sync-time'
.col-sm-12 .col-sm-12
%hr %hr
.col-lg-3 .col-lg-3
......
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
......
%td %td
= render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: protected_branch.merge_access_level_frequencies, input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' } = render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' }
%td %td
= render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: protected_branch.push_access_level_frequencies, input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' } = render partial: 'projects/protected_branches/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' }
- default_label = 'Select'
- dropdown_label = default_label
%div{ class: "#{input_basic_name}-container" }
- if access_levels.present?
- dropdown_label = [pluralize(level_frequencies[:role], 'role'), pluralize(level_frequencies[:user], 'user'), pluralize(level_frequencies[:group], 'group')].to_sentence
= dropdown_tag(dropdown_label, options: { toggle_class: "#{toggle_class} js-multiselect", dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { default_label: default_label, preselected_items: access_levels_data(access_levels) } })
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
...@@ -24,9 +24,13 @@ ...@@ -24,9 +24,13 @@
.col-md-10 .col-md-10
.create_access_levels-container .create_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide', options: { toggle_class: 'js-allowed-to-create js-multiselect wide',
dropdown_class: 'dropdown-menu-selectable', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) data: { input_id: 'create_access_levels_attributes', default_label: 'Select' } })
.help-block
Only groups that
= link_to 'have this project shared', help_page_path('workflow/share_projects_with_other_groups')
can be added here
.panel-footer .panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true = f.submit 'Protect', class: 'btn-create btn', disabled: true
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
%ul.dropdown-footer-list %ul.dropdown-footer-list
%li %li
= link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do %button{ class: "create-new-protected-tag-button js-create-new-protected-tag", title: "New Protected Tag" }
Create wildcard Create wildcard
%code %code
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_tags') = page_specific_javascript_bundle_tag('protected_tags')
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } }
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
Protected tags Protected Tags
%p.prepend-top-20 %p.prepend-top-20
By default, Protected tags are designed to: By default, protected tags are designed to:
%ul %ul
%li Prevent tag creation by everybody except Masters %li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag %li Prevent <strong>anyone</strong> from deleting the tag
%p.append-bottom-0 Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
.col-lg-9 .col-lg-9
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag' = render 'projects/protected_tags/create_protected_tag'
......
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
- else - else
(tag was removed from repository) (tag was removed from repository)
= render partial: 'projects/protected_tags/update_protected_tag', locals: { protected_tag: protected_tag } = render partial: 'projects/protected_tags/protected_tag_access_summary', locals: { protected_tag: protected_tag }
- if can_admin_project - if can_admin_project
%td %td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
%td
= render partial: 'projects/protected_tags/access_level_dropdown', locals: { protected_tag: protected_tag, access_levels: protected_tag.create_access_levels, level_frequencies: access_level_frequencies(protected_tag.create_access_levels), input_basic_name: 'create_access_levels', toggle_class: 'js-allowed-to-create' }
.panel.panel-default.protected-tags-list.js-protected-tags-list .panel.panel-default.protected-tags-list
- if @protected_tags.empty? - if @protected_tags.empty?
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
%col{ width: "25%" } %col{ width: "25%" }
%col{ width: "25%" } %col{ width: "25%" }
%col{ width: "50%" } %col{ width: "50%" }
- if can_admin_project
%col
%thead %thead
%tr %tr
%th Protected tag (#{@protected_tags.size}) %th Protected tag (#{@protected_tags.size})
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%h4.prepend-top-0.ref-name %h4.prepend-top-0.ref-name
= @protected_ref.name = @protected_ref.name
.col-lg-9 .col-lg-9.edit_protected_tag
%h5 Matching Tags %h5 Matching Tags
- if @matching_refs.present? - if @matching_refs.present?
.table-responsive .table-responsive
......
...@@ -4,8 +4,13 @@ ...@@ -4,8 +4,13 @@
%span.btn.disabled %span.btn.disabled
= icon("refresh spin") = icon("refresh spin")
Updating&hellip; Updating&hellip;
- elsif !can_force_update_mirror?(@project)
%span.btn.disabled{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'You can only force update once every five minutes.' }
= icon("refresh")
Update Now
- else - else
= link_to update_now_namespace_project_mirror_path(@project.namespace, @project), method: :post, class: "btn" do = link_to update_now_namespace_project_mirror_path(@project.namespace, @project), method: :post, class: 'btn' do
= icon("refresh") = icon("refresh")
Update Now Update Now
- if @project.mirror_last_update_success? - if @project.mirror_last_update_success?
......
...@@ -19,13 +19,13 @@ ...@@ -19,13 +19,13 @@
.col-sm-10 .col-sm-10
- author = issuable.author || current_user - author = issuable.author || current_user
- skip_users = issuable.all_approvers_including_groups + [author] - skip_users = issuable.all_approvers_including_groups + [author]
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', email_user: true, skip_users: skip_users) = users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', email_user: true, skip_users: skip_users, project: issuable.target_project)
.help-block .help-block
This merge request must be approved by these users. This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers. You can override the project settings by setting your own list of approvers.
- skip_groups = issuable.overall_approver_groups.pluck(:group_id) - skip_groups = issuable.overall_approver_groups.pluck(:group_id)
= groups_select_tag('merge_request[approver_group_ids]', multiple: true, data: { skip_groups: skip_groups, all_available: true }, class: 'input-large') = groups_select_tag('merge_request[approver_group_ids]', multiple: true, data: { skip_groups: skip_groups, all_available: true, project: issuable.target_project }, class: 'input-large')
.help-block .help-block
This merge request must be approved by members of these groups. This merge request must be approved by members of these groups.
You can override the project settings by setting your own list of approvers. You can override the project settings by setting your own list of approvers.
......
class RepositoryForkWorker class RepositoryForkWorker
ForkError = Class.new(StandardError)
include Sidekiq::Worker include Sidekiq::Worker
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -8,29 +10,31 @@ class RepositoryForkWorker ...@@ -8,29 +10,31 @@ class RepositoryForkWorker
source_path: source_path, source_path: source_path,
target_path: target_path) target_path: target_path)
project = Project.find_by_id(project_id) project = Project.find(project_id)
project.import_start
unless project.present?
logger.error("Project #{project_id} no longer exists!")
return
end
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path) project.repository_storage_path, target_path)
unless result raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
project.mark_import_as_failed('The project could not be forked.')
return
end
project.repository.after_import project.repository.after_import
raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
unless project.valid_repo?
logger.error("Project #{project_id} had an invalid repository after fork")
project.mark_import_as_failed('The forked repository is invalid.')
return
end
project.import_finish project.import_finish
rescue ForkError => ex
fail_fork(project, ex.message)
raise
rescue => ex
return unless project
fail_fork(project, ex.message)
raise ForkError, "#{ex.class} #{ex.message}"
end
private
def fail_fork(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
end end
end end
class RepositoryImportWorker class RepositoryImportWorker
ImportError = Class.new(StandardError)
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -10,6 +12,8 @@ class RepositoryImportWorker ...@@ -10,6 +12,8 @@ class RepositoryImportWorker
@project = Project.find(project_id) @project = Project.find(project_id)
@current_user = @project.creator @current_user = @project.creator
project.import_start
Gitlab::Metrics.add_event(:import_repository, Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url, import_url: @project.import_url,
path: @project.path_with_namespace) path: @project.path_with_namespace)
...@@ -17,16 +21,27 @@ class RepositoryImportWorker ...@@ -17,16 +21,27 @@ class RepositoryImportWorker
project.update_columns(import_jid: self.jid, import_error: nil) project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute result = Projects::ImportService.new(project, current_user).execute
raise ImportError, result[:message] if result[:status] == :error
if result[:status] == :error
project.mark_import_as_failed(result[:message])
return
end
project.repository.after_import project.repository.after_import
project.import_finish project.import_finish
# Explicitly update mirror so that upstream remote is created and fetched # Explicitly schedule mirror for update so
project.update_mirror # that upstream remote is created and fetched
project.import_schedule if project.mirror?
rescue ImportError => ex
fail_import(project, ex.message)
raise
rescue => ex
return unless project
fail_import(project, ex.message)
raise ImportError, "#{ex.class} #{ex.message}"
end
private
def fail_import(project, message)
project.mark_import_as_failed(message)
end end
end end
class RepositoryUpdateMirrorDispatchWorker
include Sidekiq::Worker
LEASE_TIMEOUT = 5.minutes
sidekiq_options queue: :project_mirror
attr_accessor :project, :repository, :current_user
def perform(project_id)
return unless try_obtain_lease(project_id)
@project = Project.find_by_id(project_id)
return unless project
project.update_mirror
end
private
def try_obtain_lease(project_id)
# Using 5 minutes timeout based on the 95th percent of timings (currently max of 25 minutes)
lease = ::Gitlab::ExclusiveLease.new("repository_update_mirror_dispatcher:#{project_id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end
class RepositoryUpdateMirrorWorker class RepositoryUpdateMirrorWorker
UpdateMirrorError = Class.new(StandardError) UpdateError = Class.new(StandardError)
UpdateAlreadyInProgressError = Class.new(StandardError)
include Sidekiq::Worker include Sidekiq::Worker
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
...@@ -10,25 +11,35 @@ class RepositoryUpdateMirrorWorker ...@@ -10,25 +11,35 @@ class RepositoryUpdateMirrorWorker
attr_accessor :project, :repository, :current_user attr_accessor :project, :repository, :current_user
def perform(project_id) def perform(project_id)
begin project = Project.find(project_id)
project = Project.find(project_id)
raise UpdateAlreadyInProgressError if project.import_started?
return unless project project.import_start
@current_user = project.mirror_user || project.creator @current_user = project.mirror_user || project.creator
result = Projects::UpdateMirrorService.new(project, @current_user).execute result = Projects::UpdateMirrorService.new(project, @current_user).execute
if result[:status] == :error raise UpdateError, result[:message] if result[:status] == :error
project.mark_import_as_failed(result[:message])
return project.import_finish
end rescue UpdateAlreadyInProgressError
raise
project.import_finish rescue UpdateError => ex
rescue => ex fail_mirror(project, ex.message)
if project raise
project.mark_import_as_failed("We're sorry, a temporary error occurred, please try again.") rescue => ex
raise UpdateMirrorError, "#{ex.class}: #{Gitlab::UrlSanitizer.sanitize(ex.message)}" return unless project
end
end fail_mirror(project, ex.message)
raise UpdateError, "#{ex.class}: #{ex.message}"
ensure
UpdateAllMirrorsWorker.perform_async if Gitlab::Mirror.threshold_reached?
end
private
def fail_mirror(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
end end
end end
...@@ -23,15 +23,25 @@ class RepositoryUpdateRemoteMirrorWorker ...@@ -23,15 +23,25 @@ class RepositoryUpdateRemoteMirrorWorker
project = remote_mirror.project project = remote_mirror.project
current_user = project.creator current_user = project.creator
result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror) result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
raise UpdateError, result[:message] if result[:status] == :error raise UpdateError, result[:message] if result[:status] == :error
remote_mirror.update_finish remote_mirror.update_finish
rescue UpdateAlreadyInProgressError rescue UpdateAlreadyInProgressError
raise raise
rescue UpdateError => ex rescue UpdateError => ex
remote_mirror.mark_as_failed(Gitlab::UrlSanitizer.sanitize(ex.message)) fail_remote_mirror(remote_mirror, ex.message)
raise raise
rescue => ex rescue => ex
return unless remote_mirror
fail_remote_mirror(remote_mirror, ex.message)
raise UpdateError, "#{ex.class}: #{ex.message}" raise UpdateError, "#{ex.class}: #{ex.message}"
end end
private
def fail_remote_mirror(remote_mirror, message)
Rails.logger.error(message)
remote_mirror.mark_as_failed(message)
end
end end
...@@ -2,39 +2,38 @@ class UpdateAllMirrorsWorker ...@@ -2,39 +2,38 @@ class UpdateAllMirrorsWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include CronjobQueue
LEASE_TIMEOUT = 840 LEASE_TIMEOUT = 5.minutes
LEASE_KEY = 'update_all_mirrors'.freeze
def perform def perform
# This worker requires updating the database state, which we can't # This worker requires updating the database state, which we can't
# do on a Geo secondary # do on a Geo secondary
return if Gitlab::Geo.secondary? return if Gitlab::Geo.secondary?
return unless try_obtain_lease
lease_uuid = try_obtain_lease
return unless lease_uuid
fail_stuck_mirrors! fail_stuck_mirrors!
mirrors_to_sync.find_each(batch_size: 200) do |project| return if Gitlab::Mirror.max_mirror_capacity_reached?
RepositoryUpdateMirrorDispatchWorker.perform_in(rand((project.sync_time / 2).minutes), project.id) Project.mirrors_to_sync.find_each(batch_size: 200, &:import_schedule)
end
cancel_lease(lease_uuid)
end end
def fail_stuck_mirrors! def fail_stuck_mirrors!
stuck = Project.mirror Project.stuck_mirrors.find_each(batch_size: 50) do |project|
.with_import_status(:started)
.where('mirror_last_update_at < ?', 2.hours.ago)
stuck.find_each(batch_size: 50) do |project|
project.mark_import_as_failed('The mirror update took too long to complete.') project.mark_import_as_failed('The mirror update took too long to complete.')
end end
end end
private private
def mirrors_to_sync def try_obtain_lease
Project.mirror.where("mirror_last_successful_update_at + #{Gitlab::Database.minute_interval('sync_time')} <= ? OR sync_time IN (?)", DateTime.now, Gitlab::Mirror.sync_times) ::Gitlab::ExclusiveLease.new(LEASE_KEY, timeout: LEASE_TIMEOUT).try_obtain
end end
def try_obtain_lease def cancel_lease(uuid)
lease = ::Gitlab::ExclusiveLease.new("update_all_mirrors", timeout: LEASE_TIMEOUT) ::Gitlab::ExclusiveLease.cancel(LEASE_KEY, uuid)
lease.try_obtain
end end
end end
---
title: Per user/group access levels for Protected Tags
merge_request: 1629
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.
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