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:
Style/ParenthesesAroundCondition:
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.
Style/RedundantException:
Enabled: true
......
......@@ -236,13 +236,6 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: false
# Offense count: 65
# Cop supports --auto-correct.
# 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';
// EE-only
import ApproversSelect from './approvers_select';
import AuditLogs from './audit_logs';
(function() {
var Dispatcher;
......@@ -392,6 +393,9 @@ import ApproversSelect from './approvers_select';
case 'admin:emails:show':
new AdminEmailSelect();
break;
case 'admin:audit_logs:index':
new AuditLogs();
break;
case 'projects:repository:show':
// Initialize Protected Branch Settings
new gl.ProtectedBranchCreate();
......
......@@ -56,6 +56,8 @@
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
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') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} 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 */
import Api from './api';
(function() {
this.ProjectSelect = (function() {
(function () {
this.ProjectSelect = (function () {
function ProjectSelect() {
$('.js-projects-dropdown-toggle').each(function(i, dropdown) {
$('.js-projects-dropdown-toggle').each(function (i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
......@@ -13,16 +13,16 @@ import Api from './api';
search: {
fields: ['name_with_namespace']
},
data: function(term, callback) {
data: function (term, callback) {
var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
finalCallback = function (projects) {
return callback(projects);
};
if (this.includeGroups) {
projectsCallback = function(projects) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function(groups) {
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
......@@ -35,24 +35,32 @@ import Api from './api';
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} 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;
},
text: function(project) {
text: function (project) {
return project.name_with_namespace;
}
});
});
$('.ajax-project-select').each(function(i, select) {
$('.ajax-project-select').each(function (i, select) {
var placeholder;
var idAttribute;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('allprojects') || false;
this.orderBy = $(select).data('order-by') || 'id';
<<<<<<< HEAD
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
=======
idAttribute = $(select).data('idattribute') || 'web_url';
>>>>>>> origin/master
placeholder = "Search for project";
if (this.includeGroups) {
......@@ -61,10 +69,10 @@ import Api from './api';
return $(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
return function(query) {
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function(projects) {
finalCallback = function (projects) {
var data;
data = {
results: projects
......@@ -72,9 +80,9 @@ import Api from './api';
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function(projects) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function(groups) {
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
......@@ -89,16 +97,20 @@ import Api from './api';
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
<<<<<<< HEAD
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled
=======
membership: !_this.allProjects
>>>>>>> origin/master
}, projectsCallback);
}
};
})(this),
id: function(project) {
return project.web_url;
id: function (project) {
return project[idAttribute];
},
text: function(project) {
text: function (project) {
return project.name_with_namespace || project.name;
},
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 ProtectedTagDropdown from './protected_tag_dropdown';
......@@ -5,6 +8,12 @@ export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
......@@ -14,15 +23,13 @@ export default class ProtectedTagCreate {
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
data: gon.create_access_levels,
accessLevelsData: gon.create_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
});
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
......@@ -30,12 +37,60 @@ export default class ProtectedTagCreate {
});
}
// This will run after clicked callback
// Enable submit button after selecting an option
onSelect() {
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length);
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 {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
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.bindEvents();
......@@ -73,7 +73,7 @@ export default class ProtectedTagDropdown {
};
this.$dropdownContainer
.find('.create-new-protected-tag code')
.find('.js-create-new-protected-tag code')
.text(tagName);
}
......
/* eslint-disable no-new */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap;
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();
}
buildDropdowns() {
// 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,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
});
}
onSelect() {
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
this.hasChanges = true;
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',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
protected_tag: formData,
},
success: (response) => {
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() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
$.scrollTo(0);
new Flash('Failed to update tag!');
},
}).always(() => {
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 {
<span v-for="approver in approvedBy">
<link-to-member-avatar
:avatarSize="20"
:avatar-url="approver.user.avatar_url"
extra-link-class="approver-avatar"
:display-name="approver.user.name"
: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';
import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import RebaseState from './components/states/mr_widget_rebase';
import WidgetCodeQuality from './components/mr_widget_code_quality.vue';
export default {
extends: CEWidgetOptions,
......@@ -9,11 +10,16 @@ export default {
'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode,
'mr-widget-rebase': RebaseState,
'mr-widget-code-quality': WidgetCodeQuality,
},
computed: {
shouldRenderApprovals() {
return this.mr.approvalsRequired;
},
shouldRenderCodeQuality() {
const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path;
},
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -29,6 +35,11 @@ export default {
v-if="mr.approvalsRequired"
:mr="mr"
:service="service" />
<mr-widget-code-quality
v-if="shouldRenderCodeQuality"
:mr="mr"
:service="service"
/>
<component
:is="componentName"
:mr="mr"
......
......@@ -28,4 +28,8 @@ export default class MRWidgetService extends CEWidgetService {
rebase() {
return this.rebaseResource.save();
}
fetchCodeclimate(endpoint) { // eslint-disable-line
return Vue.http.get(endpoint);
}
}
import CEMergeRequestStore from '../../stores/mr_widget_store';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
this.initCodeclimate(data);
}
setData(data) {
this.initGeo(data);
this.initSquashBeforeMerge(data);
......@@ -43,4 +48,33 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.isApproved = !this.approvalsLeft || false;
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 {
cb.call(null, res);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
.catch(() => new Flash('Something went wrong. Please try again.'));
},
initPolling() {
this.pollingInterval = new gl.SmartInterval({
......@@ -137,9 +135,7 @@ export default {
document.body.appendChild(el);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
.catch(() => new Flash('Something went wrong. Please try again.'));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
......
......@@ -2,10 +2,12 @@ import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
this.sha = data.diff_head_sha;
<<<<<<< HEAD
this.gitlabLogo = data.gitlabLogo;
=======
>>>>>>> origin/master
this.setData(data);
}
......@@ -137,5 +139,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(event.updated_at);
}
}
......@@ -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 {
}
}
.new_protected_branch {
a.allowed-to-merge,
a.allowed-to-push {
cursor: pointer;
}
.new-protected-branch,
.new-protected-tag {
label {
margin-top: 6px;
font-weight: normal;
}
}
a.allowed-to-merge,
a.allowed-to-push {
cursor: pointer;
}
.protected-branch-push-access-list,
.protected-branch-merge-access-list {
a {
......@@ -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;
width: 100%;
......
......@@ -172,10 +172,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:elasticsearch_search,
:repository_size_limit,
:shared_runners_minutes,
:minimum_mirror_sync_time,
:geo_status_timeout,
:elasticsearch_experimental_indexer,
:check_namespace_plan
:check_namespace_plan,
:mirror_max_delay,
:mirror_max_capacity,
:mirror_capacity_threshold
]
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
def create
if @project.update_attributes(import_params)
@project.reload
if @project.import_failed?
@project.import_retry
else
@project.import_start
@project.add_import_job
end
@project.reload.import_schedule
end
redirect_to namespace_project_import_path(@project.namespace, @project)
......
......@@ -14,10 +14,10 @@ class Projects::MirrorsController < Projects::ApplicationController
def update
if @project.update_attributes(mirror_params)
if @project.mirror?
@project.update_mirror
@project.force_import_job!
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."
else
flash[:notice] = "Mirroring settings were successfully updated."
......@@ -34,9 +34,10 @@ class Projects::MirrorsController < Projects::ApplicationController
@project.update_remote_mirrors
flash[:notice] = "The remote repository is being updated..."
else
@project.update_mirror
@project.force_import_job!
flash[:notice] = "The repository is being updated..."
end
redirect_to_repository_settings(@project)
end
......@@ -48,7 +49,6 @@ class Projects::MirrorsController < Projects::ApplicationController
def mirror_params
params.require(:project).permit(:mirror, :import_url, :mirror_user_id,
:mirror_trigger_builds, :sync_time,
remote_mirrors_attributes: [:url, :id, :enabled])
:mirror_trigger_builds, remote_mirrors_attributes: [:url, :id, :enabled])
end
end
......@@ -23,7 +23,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def protected_ref_params
params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id, :user_id, :_destroy, :group_id],
push_access_levels_attributes: [:access_level, :id, :user_id, :_destroy, :group_id])
merge_access_levels_attributes: access_level_attributes,
push_access_levels_attributes: access_level_attributes)
end
end
......@@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
format.js { head :ok }
end
end
protected
def access_level_attributes
%i(access_level id user_id _destroy group_id)
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
end
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
......@@ -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_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),
push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
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
private
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
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
ProtectedBranch.protected?(project, branch.name)
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)
access_levels.map do |level|
if level.type == :user
......
module DropdownsHelper
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" }
if options.has_key?(:data)
if options.key?(:data)
data_attr = options[:data].merge(data_attr)
end
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)
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 = ""
if options.has_key?(:title)
if options.key?(:title)
output << dropdown_title(options[:title])
end
if options.has_key?(:filter)
if options.key?(:filter)
output << dropdown_filter(options[:placeholder])
end
output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.has_key?(:content_class)}") do
capture(&block) if block && !options.has_key?(:footer_content)
output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do
capture(&block) if block && !options.key?(:footer_content)
end
if block && options[:footer_content]
......@@ -45,7 +45,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {})
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 << icon('chevron-down')
output.html_safe
......@@ -53,7 +53,7 @@ module DropdownsHelper
end
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
end
......
......@@ -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
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
......@@ -432,6 +432,12 @@ module ProjectsHelper
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?
if @project.group && @project.group.membership_lock
true
......
......@@ -154,13 +154,9 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
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|
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")
end
end
......@@ -168,7 +164,7 @@ class ApplicationSetting < ActiveRecord::Base
validates_each :import_sources do |record, attr, value|
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")
end
end
......@@ -186,8 +182,6 @@ class ApplicationSetting < ActiveRecord::Base
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
after_update :update_mirror_cron_job, if: :minimum_mirror_sync_time_changed?
after_commit do
Rails.cache.write(CACHE_KEY, self)
end
......@@ -218,7 +212,7 @@ class ApplicationSetting < ActiveRecord::Base
ApplicationSetting.define_attribute_methods
end
def self.defaults_ce
def self.defaults
{
after_sign_up_text: nil,
akismet_enabled: false,
......@@ -269,20 +263,6 @@ class ApplicationSetting < ActiveRecord::Base
}
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
create(defaults)
end
......@@ -295,13 +275,6 @@ class ApplicationSetting < ActiveRecord::Base
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
read_attribute(:elasticsearch_url).split(',').map(&:strip)
end
......
......@@ -9,11 +9,15 @@ class AuditEvent < ActiveRecord::Base
after_initialize :initialize_details
def author_name
details[:author_name].blank? ? user&.name : details[:author_name]
end
def initialize_details
self.details = {} if details.nil?
end
def author_name
self.user.try(:name) || details[:author_name]
def present
AuditEventPresenter.new(self)
end
end
......@@ -35,6 +35,7 @@ module Ci
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual).relevant }
scope :codeclimate, ->() { where(name: 'codeclimate') }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
......@@ -48,8 +49,8 @@ module Ci
before_destroy { unscoped_project }
after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
after_destroy :update_project_statistics
after_commit :update_project_statistics_after_save, on: [:create, :update]
after_commit :update_project_statistics, on: :destroy
class << self
# This is needed for url_for to work,
......@@ -414,6 +415,11 @@ module Ci
trace
end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
private
def update_artifacts_size
......@@ -492,5 +498,11 @@ module Ci
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
def update_project_statistics_after_save
if previous_changes.include?('artifacts_size')
update_project_statistics
end
end
end
end
......@@ -403,6 +403,10 @@ module Ci
.fabricate!
end
def codeclimate_artifact
artifacts.codeclimate.find(&:has_codeclimate_json?)
end
private
def pipeline_data
......
......@@ -177,7 +177,7 @@ class Commit
if RequestStore.active?
key = "commit_author:#{author_email.downcase}"
# 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]
else
@author = find_author_by_any_email
......
......@@ -18,7 +18,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true
alias_attribute :author, :user
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
......@@ -83,14 +83,15 @@ class CommitStatus < ActiveRecord::Base
next if transition.loopback?
commit_status.run_after_commit do
pipeline.try do |pipeline|
if pipeline
if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
end
ExpireJobCacheWorker.perform_async(commit_status.id)
end
ExpireJobCacheWorker.perform_async(commit_status.id)
end
end
......
......@@ -36,20 +36,16 @@ module Approvable
#
def number_of_potential_approvers
has_access = ['access_level > ?', Member::REPORTER]
all_approvers = all_approvers_including_groups
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?
wheres << "id IN (#{all_approvers.map(&:id).join(', ')})"
end
if project.group
wheres << "id IN (#{project.group.members.where(has_access).select(:user_id).to_sql})"
end
users = User
.active
.where("(#{wheres.join(' OR ')}) AND id NOT IN (#{approvals.select(:user_id).to_sql})")
......@@ -73,7 +69,6 @@ module Approvable
#
def overall_approvers
approvers_relation = approvers_overwritten? ? approvers : target_project.approvers
approvers_relation = approvers_relation.where.not(user_id: author.id) if author
approvers_relation
......@@ -113,7 +108,7 @@ module Approvable
end
def approvers_overwritten?
approvers.any? || approver_groups.any?
approvers.to_a.any? || approver_groups.to_a.any?
end
def can_approve?(user)
......
......@@ -3,7 +3,7 @@ module ProtectedBranchAccess
included do
include ProtectedRefAccess
include EE::ProtectedBranchAccess
include EE::ProtectedRefAccess
belongs_to :protected_branch
......
......@@ -8,32 +8,50 @@ module ProtectedRef
validates :project, presence: true
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_level.check_access(user)
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_level.access_level == Gitlab::Access::DEVELOPER
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
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)
end
end
def commit
project.commit(self.name)
end
private
def ref_matcher
......
......@@ -3,6 +3,7 @@ module ProtectedTagAccess
included do
include ProtectedRefAccess
include EE::ProtectedRefAccess
belongs_to :protected_tag
......
......@@ -7,12 +7,54 @@ module EE
extend ActiveSupport::Concern
prepended do
include IgnorableColumn
ignore_column :minimum_mirror_sync_time
validates :shared_runners_minutes,
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
def should_check_namespace_plan?
check_namespace_plan? && (::Gitlab.com? || Rails.env.development?)
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
......@@ -7,8 +7,28 @@ module EE
extend ActiveSupport::Concern
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 :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,
to: :statistics, allow_nil: true
......@@ -44,6 +64,19 @@ module EE
config.address&.gsub(wildcard, full_path)
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
def licensed_feature_available?(feature)
......@@ -57,6 +90,10 @@ module EE
end
end
def destroy_mirror_data
mirror_data.destroy
end
def 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.
# Instead, include `ProtectedBranchAccess`, which in turn includes
# this concern. A number of methods here depend on `ProtectedBranchAccess`
# being next up in the ancestor chain.
# Instead, include `ProtectedBranchAccess` or `ProtectedTagAccess`, which in
# turn include this concern. A number of methods here depend on
# `ProtectedRefAccess` being next up in the ancestor chain.
module EE
module ProtectedBranchAccess
module ProtectedRefAccess
extend ActiveSupport::Concern
included do
belongs_to :user
belongs_to :group
validates :group_id, uniqueness: { scope: :protected_branch, allow_nil: true }
validates :user_id, uniqueness: { scope: :protected_branch, allow_nil: true }
validates :access_level, uniqueness: { scope: :protected_branch, if: :role?,
protected_type = self.parent.model_name.singular
validates :group_id, uniqueness: { scope: protected_type, allow_nil: true }
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) } }
scope :by_user, -> (user) { where(user: user ) }
......
......@@ -15,6 +15,9 @@ module EE
# column directly.
validate :auditor_requires_license_add_on, if: :auditor
validate :cannot_be_admin_and_auditor
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
to: :namespace
end
module ClassMethods
......
......@@ -95,6 +95,18 @@ class GeoNode < ActiveRecord::Base
self.primary? ? false : !oauth_application.present?
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
def geo_api_url(suffix)
......@@ -134,6 +146,7 @@ class GeoNode < ActiveRecord::Base
if self.primary?
self.oauth_application = nil
update_clone_url
else
update_oauth_application!
update_system_hook!
......@@ -149,6 +162,10 @@ class GeoNode < ActiveRecord::Base
end
end
def update_clone_url
self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix
end
def update_oauth_application!
self.build_oauth_application if oauth_application.nil?
self.oauth_application.name = "Geo node: #{self.url}"
......
......@@ -271,9 +271,9 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
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(
project: project,
only: [:id, :title, :description, :color, :priority],
......
......@@ -5,8 +5,7 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :assignee, class_name: "User", foreign_key: :user_id
# EE-specific
after_create :update_elasticsearch_index
after_destroy :update_elasticsearch_index
after_commit :update_elasticsearch_index, on: [:create, :destroy]
# EE-specific
def update_elasticsearch_index
......
require 'digest/md5'
class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i
......@@ -27,10 +26,10 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true
after_create :add_to_shell
after_create :notify_user
after_commit :add_to_shell, on: :create
after_commit :notify_user, on: :create
after_create :post_create_hook
after_destroy :remove_from_shell
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
def key=(value)
......@@ -95,6 +94,6 @@ class Key < ActiveRecord::Base
end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
NotificationService.new.new_key(self)
end
end
......@@ -172,7 +172,7 @@ class Label < ActiveRecord::Base
def as_json(options = {})
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
......
......@@ -6,8 +6,7 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true
after_create :update_project_statistics
after_destroy :update_project_statistics
after_commit :update_project_statistics, on: [:create, :destroy]
private
......
......@@ -28,7 +28,7 @@ class List < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:label)
if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
......
......@@ -34,6 +34,9 @@ class MergeRequest < ActiveRecord::Base
delegate :commits, :real_size, :commits_sha, :commits_count,
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
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
......@@ -967,4 +970,13 @@ class MergeRequest < ActiveRecord::Base
true
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
......@@ -7,6 +7,7 @@ class Namespace < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::CurrentSettings
include Routable
include AfterCommitQueue
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
......@@ -247,7 +248,9 @@ class Namespace < ActiveRecord::Base
# Remove namespace directroy async with delay so
# 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
......
......@@ -170,7 +170,7 @@ class Project < ActiveRecord::Base
has_many :audit_events, as: :entity, dependent: :destroy
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 :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy
......@@ -239,10 +239,6 @@ class Project < ActiveRecord::Base
validates :repository_size_limit,
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|
project.validates :import_url, presence: true
project.validates :mirror_user, presence: true
......@@ -273,7 +269,6 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
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_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
......@@ -327,8 +322,16 @@ class Project < ActiveRecord::Base
scope :excluding_project, ->(project) { where.not(id: project) }
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
transition [:none, :finished] => :started
transition scheduled: :started
end
event :import_finish do
......@@ -336,24 +339,57 @@ class Project < ActiveRecord::Base
end
event :import_fail do
transition started: :failed
end
event :import_retry do
transition failed: :started
transition [:scheduled, :started] => :failed
end
state :scheduled
state :started
state :finished
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?
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_successful_update_at = timestamp
mirror_data = project.mirror_data
mirror_data.reset_retry_count!
mirror_data.set_next_execution_timestamp!
end
if current_application_settings.elasticsearch_indexing?
......@@ -361,8 +397,10 @@ class Project < ActiveRecord::Base
end
end
before_transition started: :failed do |project, transaction|
project.mirror_last_update_at = DateTime.now if project.mirror?
after_transition started: :finished, do: :reset_cache_and_import_attrs
after_transition [:finished, :failed] => [:scheduled, :started] do |project, _|
Gitlab::Mirror.increment_capacity(project.id) if project.mirror?
end
end
......@@ -516,7 +554,9 @@ class Project < ActiveRecord::Base
end
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?
end
......@@ -575,9 +615,17 @@ class Project < ActiveRecord::Base
end
def import_in_progress?
import_started? || import_scheduled?
end
def import_started?
import? && import_status == 'started'
end
def import_scheduled?
import_status == 'scheduled'
end
def import_failed?
import_status == 'failed'
end
......@@ -595,7 +643,10 @@ class Project < ActiveRecord::Base
end
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
def mirror_last_update_status
......@@ -620,20 +671,6 @@ class Project < ActiveRecord::Base
mirror_updated? && self.mirror_last_successful_update_at
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?
remote_mirrors.enabled.exists?
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
include Gitlab::ShellAdapter
include ProtectedRef
has_many :merge_access_levels, dependent: :destroy
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
protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
......
......@@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
has_many :create_access_levels, dependent: :destroy
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :create_access_levels
protected_ref_access_levels :create
def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present?
......
......@@ -68,7 +68,7 @@ class User < ActiveRecord::Base
#
# 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
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
merge_request)
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
delegate :current_user, to: :request
......
......@@ -29,7 +29,7 @@ class AuditEventService
target_type: "User",
target_details: user_name
}
when :update
when :update, :override
{
change: "access_level",
from: old_access_level,
......@@ -87,7 +87,8 @@ class AuditEventService
author_id: @author.id,
entity_id: @entity.id,
entity_type: @entity.class.name,
details: @details
details: @details.merge(ip_address: @author.current_sign_in_ip,
entity_path: @entity.full_path)
)
end
end
......@@ -93,7 +93,7 @@ module Geo
end
def primary_ssh_path_prefix
Gitlab::Geo.primary_ssh_path_prefix
Gitlab::Geo.primary_node.clone_url_prefix
end
def ssh_url_to_repo
......
......@@ -148,7 +148,7 @@ class IssuableBaseService < BaseService
execute(params[:description], issuable)
# 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)
end
......
......@@ -51,15 +51,14 @@ module Projects
save_project_and_import_data(import_data)
@project.import_start if @project.import?
after_create_actions if @project.persisted?
if @project.errors.empty?
@project.add_import_job if @project.import?
@project.import_schedule if @project.import?
else
fail(error: @project.errors.full_messages.join(', '))
end
@project
rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
......
......@@ -7,11 +7,9 @@ module Projects
DELETED_FLAG = '+deleted'.freeze
def async_execute
project.transaction do
project.update_attribute(:pending_delete, true)
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}")
end
project.update_attribute(:pending_delete, true)
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}")
end
def execute
......@@ -95,7 +93,11 @@ module Projects
if gitlab_shell.mv_repository(project.repository_storage_path, path, 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
false
end
......
......@@ -92,48 +92,77 @@ module SlashCommands
desc 'Assign'
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
params '@user'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
parse_params do |assignee_param|
users = extract_references(assignee_param, :user)
if users.empty?
users = User.where(username: assignee_param.split(' ').map(&:strip))
end
users
extract_users(assignee_param)
end
command :assign do |users|
next if users.empty?
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
@updates[:assignee_id] = users.last.id
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
"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
condition do
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :unassign do
command :unassign do |unassign_param = nil|
users = extract_users(unassign_param)
if issuable.is_a?(Issue)
@updates[:assignee_ids] = []
@updates[:assignee_ids] =
if users.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
else
@updates[:assignee_id] = nil
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'
explanation do |milestone|
"Sets the milestone to #{milestone.to_reference}." if milestone
......@@ -487,6 +516,18 @@ module SlashCommands
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)
extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
......
......@@ -26,6 +26,12 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
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]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
......
......@@ -71,10 +71,6 @@
%span.help-block#repository_size_limit_help_block
Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
.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
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
......@@ -105,6 +101,8 @@
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.
= render partial: 'repository_mirrors_form', locals: { f: f }
%fieldset
%legend Sign-up Restrictions
.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 @@
= link_to admin_requests_profiles_path, title: 'Requests Profiles' do
%span
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 @@
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
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
%span
Monitoring
......
......@@ -43,9 +43,6 @@
They need to have at least master access to this project.
- if @project.builds_enabled?
= 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
%hr
.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-heading
%h3.panel-title
......
%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
= 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-heading
%h3.panel-title
......@@ -24,9 +24,13 @@
.col-md-10
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
options: { toggle_class: 'js-allowed-to-create js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
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
= f.submit 'Protect', class: 'btn-create btn', disabled: true
......@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%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
%code
- content_for :page_specific_javascripts do
= 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
%h4.prepend-top-0
Protected tags
Protected Tags
%p.prepend-top-20
By default, Protected tags are designed to:
By default, protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating 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
- if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag'
......
......@@ -15,8 +15,8 @@
- else
(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
%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?
.panel-heading
%h3.panel-title
......@@ -13,6 +13,8 @@
%col{ width: "25%" }
%col{ width: "25%" }
%col{ width: "50%" }
- if can_admin_project
%col
%thead
%tr
%th Protected tag (#{@protected_tags.size})
......
......@@ -5,7 +5,7 @@
%h4.prepend-top-0.ref-name
= @protected_ref.name
.col-lg-9
.col-lg-9.edit_protected_tag
%h5 Matching Tags
- if @matching_refs.present?
.table-responsive
......
......@@ -4,8 +4,13 @@
%span.btn.disabled
= icon("refresh spin")
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
= 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")
Update Now
- if @project.mirror_last_update_success?
......
......@@ -19,13 +19,13 @@
.col-sm-10
- author = issuable.author || current_user
- 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
This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers.
- 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
This merge request must be approved by members of these groups.
You can override the project settings by setting your own list of approvers.
......
class RepositoryForkWorker
ForkError = Class.new(StandardError)
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
......@@ -8,29 +10,31 @@ class RepositoryForkWorker
source_path: source_path,
target_path: target_path)
project = Project.find_by_id(project_id)
unless project.present?
logger.error("Project #{project_id} no longer exists!")
return
end
project = Project.find(project_id)
project.import_start
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_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
raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
project.repository.after_import
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
raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
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
class RepositoryImportWorker
ImportError = Class.new(StandardError)
include Sidekiq::Worker
include DedicatedSidekiqQueue
......@@ -10,6 +12,8 @@ class RepositoryImportWorker
@project = Project.find(project_id)
@current_user = @project.creator
project.import_start
Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url,
path: @project.path_with_namespace)
......@@ -17,16 +21,27 @@ class RepositoryImportWorker
project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
project.mark_import_as_failed(result[:message])
return
end
raise ImportError, result[:message] if result[:status] == :error
project.repository.after_import
project.import_finish
# Explicitly update mirror so that upstream remote is created and fetched
project.update_mirror
# Explicitly schedule mirror for update so
# 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
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
UpdateMirrorError = Class.new(StandardError)
UpdateError = Class.new(StandardError)
UpdateAlreadyInProgressError = Class.new(StandardError)
include Sidekiq::Worker
include Gitlab::ShellAdapter
......@@ -10,25 +11,35 @@ class RepositoryUpdateMirrorWorker
attr_accessor :project, :repository, :current_user
def perform(project_id)
begin
project = Project.find(project_id)
return unless project
@current_user = project.mirror_user || project.creator
result = Projects::UpdateMirrorService.new(project, @current_user).execute
if result[:status] == :error
project.mark_import_as_failed(result[:message])
return
end
project.import_finish
rescue => ex
if project
project.mark_import_as_failed("We're sorry, a temporary error occurred, please try again.")
raise UpdateMirrorError, "#{ex.class}: #{Gitlab::UrlSanitizer.sanitize(ex.message)}"
end
end
project = Project.find(project_id)
raise UpdateAlreadyInProgressError if project.import_started?
project.import_start
@current_user = project.mirror_user || project.creator
result = Projects::UpdateMirrorService.new(project, @current_user).execute
raise UpdateError, result[:message] if result[:status] == :error
project.import_finish
rescue UpdateAlreadyInProgressError
raise
rescue UpdateError => ex
fail_mirror(project, ex.message)
raise
rescue => ex
return unless project
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
......@@ -23,15 +23,25 @@ class RepositoryUpdateRemoteMirrorWorker
project = remote_mirror.project
current_user = project.creator
result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
raise UpdateError, result[:message] if result[:status] == :error
remote_mirror.update_finish
rescue UpdateAlreadyInProgressError
raise
rescue UpdateError => ex
remote_mirror.mark_as_failed(Gitlab::UrlSanitizer.sanitize(ex.message))
fail_remote_mirror(remote_mirror, ex.message)
raise
rescue => ex
return unless remote_mirror
fail_remote_mirror(remote_mirror, ex.message)
raise UpdateError, "#{ex.class}: #{ex.message}"
end
private
def fail_remote_mirror(remote_mirror, message)
Rails.logger.error(message)
remote_mirror.mark_as_failed(message)
end
end
......@@ -2,39 +2,38 @@ class UpdateAllMirrorsWorker
include Sidekiq::Worker
include CronjobQueue
LEASE_TIMEOUT = 840
LEASE_TIMEOUT = 5.minutes
LEASE_KEY = 'update_all_mirrors'.freeze
def perform
# This worker requires updating the database state, which we can't
# do on a Geo secondary
return if Gitlab::Geo.secondary?
return unless try_obtain_lease
lease_uuid = try_obtain_lease
return unless lease_uuid
fail_stuck_mirrors!
mirrors_to_sync.find_each(batch_size: 200) do |project|
RepositoryUpdateMirrorDispatchWorker.perform_in(rand((project.sync_time / 2).minutes), project.id)
end
return if Gitlab::Mirror.max_mirror_capacity_reached?
Project.mirrors_to_sync.find_each(batch_size: 200, &:import_schedule)
cancel_lease(lease_uuid)
end
def fail_stuck_mirrors!
stuck = Project.mirror
.with_import_status(:started)
.where('mirror_last_update_at < ?', 2.hours.ago)
stuck.find_each(batch_size: 50) do |project|
Project.stuck_mirrors.find_each(batch_size: 50) do |project|
project.mark_import_as_failed('The mirror update took too long to complete.')
end
end
private
def mirrors_to_sync
Project.mirror.where("mirror_last_successful_update_at + #{Gitlab::Database.minute_interval('sync_time')} <= ? OR sync_time IN (?)", DateTime.now, Gitlab::Mirror.sync_times)
def try_obtain_lease
::Gitlab::ExclusiveLease.new(LEASE_KEY, timeout: LEASE_TIMEOUT).try_obtain
end
def try_obtain_lease
lease = ::Gitlab::ExclusiveLease.new("update_all_mirrors", timeout: LEASE_TIMEOUT)
lease.try_obtain
def cancel_lease(uuid)
::Gitlab::ExclusiveLease.cancel(LEASE_KEY, uuid)
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