Commit 950350f4 authored by Oswaldo Ferreira's avatar Oswaldo Ferreira

Merge branch 'master' into 'docs/fix-markdown-api-issues'

# Conflicts:
#   doc/api/issues.md
parents 61f4eb5c ce0fb87c
......@@ -31,6 +31,7 @@ eslint-report.html
/config/unicorn.rb
/config/secrets.yml
/config/sidekiq.yml
/config/registry.key
/coverage/*
/coverage-javascript/
/db/*.sqlite3
......
import * as THREE from 'three/build/three.module';
import STLLoaderClass from 'three-stl-loader';
import OrbitControlsClass from 'three-orbit-controls';
import MeshObject from './mesh_object';
const STLLoader = STLLoaderClass(THREE);
const OrbitControls = OrbitControlsClass(THREE);
export default class Renderer {
constructor(container) {
this.renderWrapper = this.render.bind(this);
this.objects = [];
this.container = container;
this.width = this.container.offsetWidth;
this.height = 500;
this.loader = new STLLoader();
this.fov = 45;
this.camera = new THREE.PerspectiveCamera(
this.fov,
this.width / this.height,
1,
1000,
);
this.scene = new THREE.Scene();
this.scene.add(this.camera);
// Setup the viewer
this.setupRenderer();
this.setupGrid();
this.setupLight();
// Setup OrbitControls
this.controls = new OrbitControls(
this.camera,
this.renderer.domElement,
);
this.controls.minDistance = 5;
this.controls.maxDistance = 30;
this.controls.enableKeys = false;
this.loadFile();
}
setupRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
});
this.renderer.setClearColor(0xFFFFFF);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(
this.width,
this.height,
);
}
setupLight() {
// Point light illuminates the object
const pointLight = new THREE.PointLight(
0xFFFFFF,
2,
0,
);
pointLight.castShadow = true;
this.camera.add(pointLight);
// Ambient light illuminates the scene
const ambientLight = new THREE.AmbientLight(
0xFFFFFF,
1,
);
this.scene.add(ambientLight);
}
setupGrid() {
this.grid = new THREE.GridHelper(
20,
20,
0x000000,
0x000000,
);
this.scene.add(this.grid);
}
loadFile() {
this.loader.load(this.container.dataset.endpoint, (geo) => {
const obj = new MeshObject(geo);
this.objects.push(obj);
this.scene.add(obj);
this.start();
this.setDefaultCameraPosition();
});
}
start() {
// Empty the container first
this.container.innerHTML = '';
// Add to DOM
this.container.appendChild(this.renderer.domElement);
// Make controls visible
this.container.parentNode.classList.remove('is-stl-loading');
this.render();
}
render() {
this.renderer.render(
this.scene,
this.camera,
);
requestAnimationFrame(this.renderWrapper);
}
changeObjectMaterials(type) {
this.objects.forEach((obj) => {
obj.changeMaterial(type);
});
}
setDefaultCameraPosition() {
const obj = this.objects[0];
const radius = (obj.geometry.boundingSphere.radius / 1.5);
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
this.camera.position.set(
0,
dist + 1,
dist,
);
this.camera.lookAt(this.grid);
this.controls.update();
}
}
import {
Matrix4,
MeshLambertMaterial,
Mesh,
} from 'three/build/three.module';
const defaultColor = 0xE24329;
const materials = {
default: new MeshLambertMaterial({
color: defaultColor,
}),
wireframe: new MeshLambertMaterial({
color: defaultColor,
wireframe: true,
}),
};
export default class MeshObject extends Mesh {
constructor(geo) {
super(
geo,
materials.default,
);
this.geometry.computeBoundingSphere();
this.rotation.set(-Math.PI / 2, 0, 0);
if (this.geometry.boundingSphere.radius > 4) {
const scale = 4 / this.geometry.boundingSphere.radius;
this.geometry.applyMatrix(
new Matrix4().makeScale(
scale,
scale,
scale,
),
);
this.geometry.computeBoundingSphere();
this.position.x = -this.geometry.boundingSphere.center.x;
this.position.z = this.geometry.boundingSphere.center.y;
}
}
changeMaterial(type) {
this.material = materials[type];
}
}
......@@ -10,7 +10,7 @@ Vue.use(PDFLab, {
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
return new Vue({
el,
data() {
return {
......
import Renderer from './3d_viewer';
document.addEventListener('DOMContentLoaded', () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
const target = e.target;
e.preventDefault();
document.querySelector('.js-material-changer.active').classList.remove('active');
target.classList.add('active');
target.blur();
viewer.changeObjectMaterials(target.dataset.type);
});
});
});
......@@ -88,6 +88,7 @@ window.Build = (function() {
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
......
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
import Clipboard from 'vendor/clipboard';
import Clipboard from 'clipboard';
var genericError, genericSuccess, showTooltip;
......
......@@ -47,8 +47,10 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -228,6 +230,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
const el = document.querySelector('.js-service-desk-setting-root');
if (el) {
const serviceDeskRoot = new ServiceDeskRoot(el);
serviceDeskRoot.init();
}
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
......@@ -238,9 +247,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
action: controllerAction,
defaultAction: 'pipelines',
......@@ -340,9 +351,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
new AdminEmailSelect();
break;
case 'projects:repository:show':
// Initialize Protected Branch Settings
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
new UsersSelect();
// Initialize Protected Tag Settings
new ProtectedTagCreate();
new ProtectedTagEditList();
break;
case 'projects:ci_cd:show':
new gl.ProjectVariables();
......
......@@ -46,11 +46,20 @@ export default {
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
<div class="btn-group" role="group">
<button
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
......@@ -59,15 +68,24 @@ export default {
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<i
class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
class="js-manual-action-link no-btn">
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>
{{action.name}}
......@@ -75,7 +93,6 @@ export default {
</button>
</li>
</ul>
</button>
</div>
`,
};
......@@ -149,6 +149,7 @@ export default {
const parsedAction = {
name: gl.text.humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
......
......@@ -12,6 +12,7 @@ class GeoNodeStatus {
this.$repositoriesSynced = $('.js-repositories-synced', this.$status);
this.$repositoriesFailed = $('.js-repositories-failed', this.$status);
this.$lfsObjectsSynced = $('.js-lfs-objects-synced', this.$status);
this.$attachmentsSynced = $('.js-attachments-synced', this.$status);
this.$health = $('.js-health', this.$status);
this.endpoint = this.$el.data('status-url');
......@@ -31,6 +32,7 @@ class GeoNodeStatus {
this.$repositoriesSynced.html(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.html(status.repositories_failed_count);
this.$lfsObjectsSynced.html(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$attachmentsSynced.html(`${status.attachments_synced_count}/${status.attachments_count} (${status.attachments_synced_in_percentage})`);
this.$health.html(status.health);
this.$status.show();
......
import Vue from 'vue';
import IssueTitle from './issue_title';
import '../vue_shared/vue_resource_interceptor';
const vueOptions = () => ({
el: '.issue-title-entrypoint',
components: {
IssueTitle,
},
data() {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
return {
initialTitle: issueTitleData.initialTitle,
endpoint: issueTitleData.endpoint,
};
},
template: `
<IssueTitle
:initialTitle="initialTitle"
:endpoint="endpoint"
/>
`,
});
(() => new Vue(vueOptions()))();
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
export default {
props: {
initialTitle: { required: true, type: String },
endpoint: { required: true, type: String },
},
data() {
const resource = new Service(this.$http, this.endpoint);
const poll = new Poll({
resource,
method: 'getTitle',
successCallback: (res) => {
this.renderResponse(res);
},
errorCallback: (err) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error('ISSUE SHOW TITLE REALTIME ERROR', err);
} else {
throw new Error(err);
}
},
});
return {
poll,
timeoutId: null,
title: this.initialTitle,
};
},
methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
},
triggerAnimation(body) {
const { title } = body;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title even on a 304 to ensure no visual change
*/
if (this.title === title) return;
this.$el.style.opacity = 0;
this.timeoutId = setTimeout(() => {
this.title = title;
this.$el.style.transition = 'opacity 0.2s ease';
this.$el.style.opacity = 1;
clearTimeout(this.timeoutId);
}, 100);
},
},
created() {
this.fetch();
},
template: `
<h2 class='title' v-html='title'></h2>
`,
};
export default class Service {
constructor(resource, endpoint) {
this.resource = resource;
this.endpoint = endpoint;
}
getTitle() {
return this.resource.get(this.endpoint);
}
}
......@@ -2,6 +2,8 @@
(function() {
(function(w) {
var base;
const faviconEl = document.getElementById('favicon');
const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
w.gl || (w.gl = {});
(base = w.gl).utils || (base.utils = {});
w.gl.utils.isInGroupsPage = function() {
......@@ -361,5 +363,34 @@
fn(next, stop);
});
};
w.gl.utils.setFavicon = (iconName) => {
if (faviconEl && iconName) {
faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
}
};
w.gl.utils.resetFavicon = () => {
if (faviconEl) {
faviconEl.setAttribute('href', originalFavicon);
}
};
w.gl.utils.setCiStatusFavicon = (pageUrl) => {
$.ajax({
url: pageUrl,
dataType: 'json',
success: function(data) {
if (data && data.icon) {
gl.utils.setFavicon(`ci_favicons/${data.icon}`);
} else {
gl.utils.resetFavicon();
}
},
error: function() {
gl.utils.resetFavicon();
}
});
};
})(window);
}).call(window);
......@@ -38,11 +38,14 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
// check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status
// check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status
// ci_status_url - String, URL to use to check CI status
// pipeline_status_url - String, URL to use to get CI status for Favicon
//
this.opts = opts;
this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({
show: false
});
......@@ -180,6 +183,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url);
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha ||
......
......@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs');
new global.LinkedTabs(options.tabsOptions);
}
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns();
}
......
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
fetchError: {
type: Error,
required: false,
default: null,
},
isInstanceAdmin: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
template: `
<div>
<div class="checkbox">
<label for="service-desk-enabled-checkbox">
<input
ref="enabled-checkbox"
type="checkbox"
id="service-desk-enabled-checkbox"
:disabled="!isInstanceAdmin"
:checked="isEnabled"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate Service Desk
</span>
</label>
</div>
<p
ref="only-instance-admin-activate-message"
v-if="!isInstanceAdmin"
class="settings-message">
Only instance admins can activate/deactivate Service Desk
</p>
<template v-if="isEnabled">
<div
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Forward external support email address to:
</h3>
</div>
<div class="panel-body">
<template v-if="fetchError">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
An error occurred while fetching the incoming email
</template>
<template v-else-if="incomingEmail">
<span
ref="service-desk-incoming-email">
{{ incomingEmail }}
</span>
<button
class="btn btn-clipboard btn-transparent has-tooltip"
title="Copy incoming email address to clipboard"
:data-clipboard-text="incomingEmail"
@click.prevent>
<i class="fa fa-clipboard" aria-hidden="true" />
</button>
</template>
<template v-else>
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
<span class="sr-only">
Fetching incoming email
</span>
</template>
</div>
</div>
<p
ref="recommend-protect-email-from-spam-message"
class="settings-message">
We recommend you protect the external support email address.
Unblocked email spam would result in many spam issues being created,
and may disrupt your GitLab service.
</p>
</template>
</div>
`,
};
import Vue from 'vue';
export default new Vue();
/* eslint-disable no-new */
import Vue from 'vue';
import ServiceDeskSetting from './components/service_desk_setting';
import ServiceDeskStore from './stores/service_desk_store';
import ServiceDeskService from './services/service_desk_service';
import eventHub from './event_hub';
class ServiceDeskRoot {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
const isEnabled = typeof this.wrapperElement.dataset.enabled !== 'undefined' &&
this.wrapperElement.dataset.enabled !== 'false';
const incomingEmail = this.wrapperElement.dataset.incomingEmail;
const endpoint = this.wrapperElement.dataset.endpoint;
const isInstanceAdmin = typeof this.wrapperElement.dataset.isInstanceAdmin !== 'undefined' &&
this.wrapperElement.dataset.isInstanceAdmin !== 'false';
this.store = new ServiceDeskStore({
isEnabled,
incomingEmail,
isInstanceAdmin,
});
this.service = new ServiceDeskService(endpoint);
}
init() {
this.bindEvents();
if (this.store.state.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
this.render();
}
bindEvents() {
this.onEnableToggledWrapper = this.onEnableToggled.bind(this);
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
unbindEvents() {
eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
render() {
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
template: `
<service-desk-setting
:isEnabled="isEnabled"
:incomingEmail="incomingEmail"
:fetchError="fetchError"
:isInstanceAdmin="isInstanceAdmin" />
`,
components: {
'service-desk-setting': ServiceDeskSetting,
},
});
}
fetchIncomingEmail() {
this.service.fetchIncomingEmail()
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
onEnableToggled(isChecked) {
this.store.setIsActivated(isChecked);
this.store.setIncomingEmail('');
this.store.setFetchError(null);
this.service.toggleServiceDesk(isChecked)
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default ServiceDeskRoot;
import Vue from 'vue';
import vueResource from 'vue-resource';
import '../../../vue_shared/vue_resource_interceptor';
Vue.use(vueResource);
class ServiceDeskService {
constructor(endpoint) {
this.serviceDeskResource = Vue.resource(`${endpoint}`);
}
fetchIncomingEmail() {
return this.serviceDeskResource.get()
.then((res) => {
const email = res.data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
toggleServiceDesk(enable) {
return this.serviceDeskResource.update({
service_desk_enabled: enable,
})
.then((res) => {
const email = res.data.service_desk_address;
if (enable && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
}
export default ServiceDeskService;
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
isEnabled: false,
incomingEmail: '',
fetchError: null,
isInstanceAdmin: false,
}, initialState);
}
setIsActivated(value) {
this.state.isEnabled = value;
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
setFetchError(value) {
this.state.fetchError = value;
}
setIsInstanceAdmin(value) {
this.state.isInstanceAdmin = value;
}
}
export default ServiceDeskStore;
export { default as ProtectedTagCreate } from './protected_tag_create';
export { default as ProtectedTagEditList } from './protected_tag_edit_list';
export default class ProtectedTagAccessDropdown {
constructor(options) {
this.options = options;
this.initDropdown();
}
initDropdown() {
const { onSelect } = this.options;
this.options.$dropdown.glDropdown({
data: this.options.data,
selectable: true,
inputId: this.options.$dropdown.data('input-id'),
fieldName: this.options.$dropdown.data('field-name'),
toggleLabel(item, $el) {
if ($el.is('.is-active')) {
return item.text;
}
return 'Select';
},
clicked(item, $el, e) {
e.preventDefault();
onSelect();
},
});
}
}
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
}
buildDropdowns() {
const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
});
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
onSelect: this.onSelectCallback,
});
}
// This will run after clicked callback
onSelect() {
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
}
export default class ProtectedTagDropdown {
/**
* @param {Object} options containing
* `$dropdown` target element
* `onSelect` event callback
* $dropdown must be an element created using `dropdown_tag()` rails helper
*/
constructor(options) {
this.onSelect = options.onSelect;
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.buildDropdown();
this.bindEvents();
// Hide footer
this.toggleFooter(true);
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedTags.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title'],
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
},
fieldName: 'protected_tag[name]',
text(protectedTag) {
return _.escape(protectedTag.title);
},
id(protectedTag) {
return _.escape(protectedTag.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (item, $el, e) => {
e.preventDefault();
this.onSelect();
},
});
}
bindEvents() {
this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
e.preventDefault();
}
getProtectedTags(term, callback) {
if (this.selectedTag) {
callback(gon.open_tags.concat(this.selectedTag));
} else {
callback(gon.open_tags);
}
}
toggleCreateNewButton(tagName) {
if (tagName) {
this.selectedTag = {
title: tagName,
id: tagName,
text: tagName,
};
this.$dropdownContainer
.find('.create-new-protected-tag code')
.text(tagName);
}
this.toggleFooter(!tagName);
}
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
}
/* eslint-disable no-new */
/* global Flash */
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.onSelectCallback = this.onSelect.bind(this);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: this.$allowedToCreateDropdownButton,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
});
}
onSelect() {
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
this.$allowedToCreateDropdownButton.disable();
$.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(),
}],
},
},
error() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
});
}
}
/* eslint-disable no-new */
import ProtectedTagEdit from './protected_tag_edit';
export default class ProtectedTagEditList {
constructor() {
this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
new ProtectedTagEdit({
$wrap: $(el),
});
});
}
}
......@@ -38,6 +38,14 @@ export default {
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
......@@ -51,16 +59,23 @@ export default {
aria-label="Manual job"
:disabled="isLoading">
${playIconSvg}
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<i
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn"
@click="onClickAction(action.path)">
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
......
/* eslint-disable no-underscore-dangle*/
import '../../vue_realtime_listener';
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore {
constructor() {
......@@ -56,6 +56,6 @@ export default class PipelinesStore {
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
VueRealtimeListener(removeIntervals, startIntervals);
}
}
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('beforeunload', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('beforeunload', removeAll);
// add removeAll methods to stack
const stack = gl.VueRealtimeListener.reset;
gl.VueRealtimeListener.reset = () => {
gl.VueRealtimeListener.reset = stack;
removeAll();
stack();
};
};
// remove all event listeners and intervals
gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {}));
export default (removeIntervals, startIntervals) => {
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
window.removeEventListener('onbeforeload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
window.addEventListener('onbeforeload', removeIntervals);
};
......@@ -276,6 +276,12 @@ span.idiff {
}
}
.is-stl-loading {
.stl-controls {
display: none;
}
}
.file-fork-suggestion {
display: flex;
align-items: center;
......
.panel {
margin-bottom: $gl-padding;
@mixin panel {
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
......@@ -48,3 +46,14 @@
line-height: inherit;
}
}
.panel {
@include panel;
margin-bottom: $gl-padding;
}
.panel-slim {
@extend .panel;
@include panel;
margin-bottom: $gl-vert-padding;
}
/**
* Container Registry
*/
.container-image {
border-bottom: 1px solid $white-normal;
}
.container-image-head {
padding: 0 16px;
line-height: 4em;
}
.table.tags {
margin-bottom: 0;
}
......@@ -757,7 +757,8 @@ a.allowed-to-push {
text-align: left;
}
.protected-branches-list {
.protected-branches-list,
.protected-tags-list {
margin-bottom: 30px;
a {
......@@ -793,6 +794,17 @@ a.allowed-to-push {
@extend .btn.disabled;
}
.protected-tags-list {
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
}
.flash-container {
padding: 0;
}
}
.custom-notifications-form {
.is-loading {
.custom-notification-event-loading {
......
......@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
......
......@@ -7,6 +7,7 @@ module ContinueParams
continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/')
return if continue_params[:to].start_with?('//')
continue_params
end
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:name]) if params[:name].present?
projects = projects.non_archived if params[:archived].blank?
projects = projects.personal(current_user) if params[:personal].present? && current_user
projects
end
end
module ParamsBackwardCompatibility
private
def set_non_archived_param
params[:non_archived] = params[:archived].blank?
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
before_action :default_sorting
def index
@projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
......@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
@projects = load_projects(current_user.viewable_starred_projects)
@projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
......@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
def default_sorting
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
end
filter_projects(projects)
def load_projects(finder_params)
ProjectsFinder.new(params: finder_params, current_user: current_user).
execute.includes(:route, namespace: :route)
end
def load_events
@events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = Event.in_projects(load_projects(params.merge(non_public: true)))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
......
......@@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@sort = params[:sort]
@todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages))
redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
end
end
......
class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
def index
@projects = load_projects
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page])
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = load_projects(Project.trending)
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
params[:trending] = true
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
@projects = load_projects
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
@projects = load_projects.reorder('star_count DESC').page(params[:page])
respond_to do |format|
format.html
......@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
protected
private
def load_projects(base_scope = nil)
base_scope ||= ProjectsFinder.new.execute(current_user)
base_scope.includes(:route, namespace: :route)
def load_projects
ProjectsFinder.new(current_user: current_user, params: params).
execute.includes(:route, namespace: :route)
end
end
......@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end
def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user)
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def authorize_admin_group!
......
class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
respond_to :html
......@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
set_non_archived_param
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
options = {}
options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1'
@projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:name].blank?
end
......
class Projects::ContainerRegistryController < Projects::ApplicationController
before_action :verify_registry_enabled
before_action :authorize_read_container_image!
before_action :authorize_update_container_image!, only: [:destroy]
layout 'project'
def index
@tags = container_registry_repository.tags
end
def destroy
url = namespace_project_container_registry_index_path(project.namespace, project)
if tag.delete
redirect_to url
else
redirect_to url, alert: 'Failed to remove tag'
end
end
private
def verify_registry_enabled
render_404 unless Gitlab.config.registry.enabled
end
def container_registry_repository
@container_registry_repository ||= project.container_registry_repository
end
def tag
@tag ||= container_registry_repository.tag(params[:id])
end
end
......@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new.execute(current_user))
@forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count
......
......@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch]
:related_branches, :can_create_branch, :rendered_title]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show]
before_action :authorize_read_issue!, only: [:show, :rendered_title]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
......@@ -32,7 +32,7 @@ class Projects::IssuesController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present?
......@@ -208,6 +208,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def rendered_title
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { title: view_context.markdown_field(@issue, :title) }
end
protected
def issue
......
......@@ -46,7 +46,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
if params[:label_name].present?
......
class Projects::ProtectedBranchesController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
protected
layout "project_settings"
def index
redirect_to_repository_settings(@project)
end
def create
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
unless @protected_branch.persisted?
flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
def project_refs
@project.repository.branches
end
def show
@matching_branches = @protected_branch.matching(@project.repository.branches)
def create_service_class
::ProtectedBranches::CreateService
end
def update
@protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
if @protected_branch.valid?
respond_to do |format|
format.json { render json: @protected_branch, status: :ok, include: [:merge_access_levels, :push_access_levels] }
end
else
respond_to do |format|
format.json { render json: @protected_branch.errors, status: :unprocessable_entity }
end
end
def update_service_class
::ProtectedBranches::UpdateService
end
def destroy
@protected_branch.destroy
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
def load_protected_ref
@protected_ref = @project.protected_branches.find(params[:id])
end
private
def load_protected_branch
@protected_branch = @project.protected_branches.find(params[:id])
def access_levels
[:merge_access_levels, :push_access_levels]
end
def protected_branch_params
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])
end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
end
class Projects::ProtectedRefsController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_ref, only: [:show, :update, :destroy]
layout "project_settings"
def index
redirect_to_repository_settings(@project)
end
def create
protected_ref = create_service_class.new(@project, current_user, protected_ref_params).execute
unless protected_ref.persisted?
flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def show
@matching_refs = @protected_ref.matching(project_refs)
end
def update
@protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref)
if @protected_ref.valid?
render json: @protected_ref, status: :ok, include: access_levels
else
render json: @protected_ref.errors, status: :unprocessable_entity
end
end
def destroy
@protected_ref.destroy
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
end
class Projects::ProtectedTagsController < Projects::ProtectedRefsController
protected
def project_refs
@project.repository.tags
end
def create_service_class
::ProtectedTags::CreateService
end
def update_service_class
::ProtectedTags::UpdateService
end
def load_protected_ref
@protected_ref = @project.protected_tags.find(params[:id])
end
def access_levels
[:create_access_levels]
end
def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id])
end
end
module Projects
module Registry
class ApplicationController < Projects::ApplicationController
layout 'project'
before_action :verify_registry_enabled!
before_action :authorize_read_container_image!
private
def verify_registry_enabled!
render_404 unless Gitlab.config.registry.enabled
end
end
end
end
module Projects
module Registry
class RepositoriesController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
before_action :ensure_root_container_repository!, only: [:index]
def index
@images = project.container_repositories
end
def destroy
if image.destroy
redirect_to project_container_registry_path(@project),
notice: 'Image repository has been removed successfully!'
else
redirect_to project_container_registry_path(@project),
alert: 'Failed to remove image repository!'
end
end
private
def image
@image ||= project.container_repositories.find(params[:id])
end
##
# Container repository object for root project path.
#
# Needed to maintain a backwards compatibility.
#
def ensure_root_container_repository!
ContainerRegistry::Path.new(@project.full_path).tap do |path|
break if path.has_repository?
ContainerRepository.build_from_path(path).tap do |repository|
repository.save! if repository.has_tags?
end
end
end
end
end
end
module Projects
module Registry
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
def destroy
if tag.delete
redirect_to project_container_registry_path(@project),
notice: 'Registry tag has been removed successfully!'
else
redirect_to project_container_registry_path(@project),
alert: 'Failed to remove registry tag!'
end
end
private
def image
@image ||= project.container_repositories
.find(params[:repository_id])
end
def tag
@tag ||= image.tag(params[:id])
end
end
end
end
class Projects::ServiceDeskController < Projects::ApplicationController
before_action :authorize_admin_instance!, only: :update
before_action :authorize_admin_project!, only: :show
def show
json_response
end
def update
Projects::UpdateService.new(project, current_user, { service_desk_enabled: params[:service_desk_enabled] }).execute
json_response
end
private
def json_response
respond_to do |format|
service_desk_attributes =
{ service_desk_address: project.service_desk_address, service_desk_enabled: project.service_desk_enabled }
format.json { render json: service_desk_attributes }
end
end
def authorize_admin_instance!
return render_404 unless current_user.is_admin?
end
end
......@@ -5,10 +5,9 @@ module Projects
before_action :remote_mirror, only: [:show]
def show
@deploy_keys = DeployKeysPresenter
.new(@project, current_user: current_user)
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
define_protected_branches
define_protected_refs
project.create_push_rule unless project.push_rule
@push_rule = project.push_rule
......@@ -16,46 +15,48 @@ module Projects
private
def define_protected_branches
load_protected_branches
@protected_branch = @project.protected_branches.new
load_gon_index
end
def remote_mirror
@remote_mirror = @project.remote_mirrors.first_or_initialize
end
def load_protected_branches
def define_protected_refs
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new
load_gon_index
end
def access_levels_options
{
push_access_levels: {
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
},
merge_access_levels: {
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
},
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 },
create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
}
end
def open_branches
branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
{ open_branches: branches }
def levels_for_dropdown(access_level_type)
roles = access_level_type.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
{ roles: roles }
end
def protectable_tags_for_dropdown
{ open_tags: ProtectableDropdown.new(@project, :tags).hash }
end
def protectable_branches_for_dropdown
{ open_branches: ProtectableDropdown.new(@project, :branches).hash }
end
def load_gon_index
params = open_branches
params[:current_project_id] = @project.id if @project
gon.push(params.merge(access_levels_options))
gon.push(protectable_tags_for_dropdown)
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
gon.push(current_project_id: @project.id) if @project
end
end
end
......
......@@ -349,6 +349,7 @@ class ProjectsController < Projects::ApplicationController
mirror_user_id
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
......
......@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
ProjectsFinder.new(current_user: current_user).execute
end
end
class GroupProjectsFinder < UnionFinder
def initialize(group, options = {})
# GroupProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# group
# options:
# only_owned: boolean
# only_shared: boolean
# params:
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class GroupProjectsFinder < ProjectsFinder
attr_reader :group, :options
def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil)
super(params: params, current_user: current_user, project_ids_relation: project_ids_relation)
@group = group
@options = options
end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private
def group_projects(current_user)
only_owned = @options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false)
def init_collection
only_owned = options.fetch(:only_owned, false)
only_shared = options.fetch(:only_shared, false)
projects = []
if current_user
if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
if group.users.include?(current_user)
projects << group.projects unless only_shared
projects << group.shared_projects unless only_owned
else
unless only_shared
projects << @group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user)
projects << group.projects.visible_to_user(current_user)
projects << group.projects.public_to_user(current_user)
end
unless only_owned
projects << @group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user)
projects << group.shared_projects.visible_to_user(current_user)
projects << group.shared_projects.public_to_user(current_user)
end
end
else
projects << @group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned
projects << group.projects.public_only unless only_shared
projects << group.shared_projects.public_only unless only_owned
end
projects
end
def union(items)
find_union(items, Project)
end
end
......@@ -121,9 +121,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group).execute(current_user)
GroupProjectsFinder.new(group: group, current_user: current_user).execute
else
projects_finder.execute(current_user, item_project_ids(items))
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
......@@ -435,8 +435,4 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end
......@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects
return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
@projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
@projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
......
# ProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# params:
# trending: boolean
# non_public: boolean
# starred: boolean
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class ProjectsFinder < UnionFinder
def execute(current_user = nil, project_ids_relation = nil)
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
attr_accessor :params
attr_reader :current_user, :project_ids_relation
find_union(segments, Project).with_route
def initialize(params: {}, current_user: nil, project_ids_relation: nil)
@params = params
@current_user = current_user
@project_ids_relation = project_ids_relation
end
def execute
items = init_collection
items = by_ids(items)
items = union(items)
items = by_personal(items)
items = by_visibilty_level(items)
items = by_tags(items)
items = by_search(items)
items = by_archived(items)
sort(items)
end
private
def all_projects(current_user)
def init_collection
projects = []
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user)
if params[:trending].present?
projects << Project.trending
elsif params[:starred].present? && current_user
projects << current_user.viewable_starred_projects
else
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
end
projects
end
def by_ids(items)
project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
end
def union(items)
find_union(items, Project).with_route
end
def by_personal(items)
(params[:personal].present? && current_user) ? items.personal(current_user) : items
end
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
def by_tags(items)
params[:tag].present? ? items.tagged_with(params[:tag]) : items
end
def by_search(items)
params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items
end
def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items
end
def by_archived(projects)
# Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
params[:non_archived] ? projects.non_archived : projects
end
end
......@@ -95,7 +95,7 @@ class TodosFinder
def projects(items)
item_project_ids = items.reorder(nil).select(:project_id)
ProjectsFinder.new.execute(current_user, item_project_ids)
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute
end
def type?
......
module BranchesHelper
def can_remove_branch?(project, branch_name)
if project.protected_branch? branch_name
if ProtectedBranch.protected?(project, branch_name)
false
elsif branch_name == project.repository.root_ref
false
......@@ -30,6 +30,10 @@ module BranchesHelper
options_for_select(@project.repository.branch_names, @project.default_branch)
end
def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name)
end
def access_levels_data(access_levels)
access_levels.map do |level|
if level.type == :user
......
......@@ -434,7 +434,10 @@ module ProjectsHelper
def sanitize_repo_path(project, message)
return '' unless message.present?
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
def project_feature_options
......
......@@ -27,8 +27,8 @@ module SortingHelper
def projects_sort_options_hash
options = {
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_latest_activity => sort_title_latest_activity,
sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created
}
......@@ -80,6 +80,14 @@ module SortingHelper
'Last updated'
end
def sort_title_oldest_activity
'Oldest updated'
end
def sort_title_latest_activity
'Last updated'
end
def sort_title_oldest_created
'Oldest created'
end
......@@ -208,6 +216,14 @@ module SortingHelper
'updated_desc'
end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_latest_activity
'latest_activity_desc'
end
def sort_value_oldest_created
'created_asc'
end
......
......@@ -21,4 +21,8 @@ module TagsHelper
html.html_safe
end
def protected_tag?(project, tag)
ProtectedTag.protected?(project, tag.name)
end
end
module Emails
module EE
module ServiceDesk
extend ActiveSupport::Concern
included do
layout 'service_desk', only: [:service_desk_thank_you_email, :service_desk_new_note_email]
end
def service_desk_thank_you_email(issue_id)
setup_service_desk_mail(issue_id)
mail_new_thread(@issue, service_desk_options(@support_bot.id))
end
def service_desk_new_note_email(issue_id, note_id)
@note = Note.find(note_id)
setup_service_desk_mail(issue_id)
mail_answer_thread(@issue, service_desk_options(@note.author_id))
end
private
def setup_service_desk_mail(issue_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@support_bot = User.support_bot
@sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
end
def service_desk_options(author_id)
{
from: sender(author_id),
to: @issue.service_desk_reply_to,
subject: "Re: #{@issue.title} (##{@issue.iid})"
}
end
end
end
end
......@@ -11,6 +11,8 @@ class Notify < BaseMailer
include Emails::Pipelines
include Emails::Members
include Emails::EE::ServiceDesk
helper MergeRequestsHelper
helper DiffHelper
helper BlobHelper
......
......@@ -58,6 +58,10 @@ class Blob < SimpleDelegator
binary? && extname.downcase.delete('.') == 'sketch'
end
def stl?
extname.downcase.delete('.') == 'stl'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -81,6 +85,8 @@ class Blob < SimpleDelegator
'notebook'
elsif sketch?
'sketch'
elsif stl?
'stl'
elsif text?
'text'
else
......
......@@ -180,22 +180,19 @@ module Elastic
def project_ids_query(current_user, project_ids, public_and_internal_projects, feature = nil)
conditions = []
limit_private_projects = {}
private_project_condition = {
bool: {
filter: {
terms: { id: project_ids }
}
}
}
if project_ids != :any
limit_private_projects[:filter] = { terms: { id: project_ids } }
end
if feature
private_project_condition[:bool][:must_not] = {
limit_private_projects[:must_not] = {
term: { "#{feature}_access_level" => ProjectFeature::DISABLED }
}
end
conditions << private_project_condition
conditions << { bool: limit_private_projects } unless limit_private_projects.empty?
if public_and_internal_projects
conditions << if feature
......
......@@ -53,7 +53,7 @@ module Elastic
end
def self.confidentiality_filter(query_hash, current_user)
return query_hash if current_user && current_user.admin?
return query_hash if current_user && current_user.admin_or_auditor?
filter = if current_user
{
......
......@@ -2,22 +2,11 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
include ProtectedRefAccess
include EE::ProtectedBranchAccess
belongs_to :protected_branch
delegate :project, to: :protected_branch
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
def humanize
self.class.human_access_levels[self.access_level]
end
def check_access(user)
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level
delegate :project, to: :protected_branch
end
end
module ProtectedRef
extend ActiveSupport::Concern
included do
belongs_to :project
validates :name, presence: true
validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
def self.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)
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:)
self.matching(ref).map(&:"#{action}_access_levels").flatten
end
def self.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
@ref_matcher ||= ProtectedRefMatcher.new(self)
end
end
module ProtectedRefAccess
extend ActiveSupport::Concern
included do
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
def humanize
self.class.human_access_levels[self.access_level]
end
def check_access(user)
return true if user.admin?
project.team.max_member_access(user.id) >= access_level
end
end
module ProtectedTagAccess
extend ActiveSupport::Concern
included do
include ProtectedRefAccess
belongs_to :protected_tag
delegate :project, to: :protected_tag
end
end
class ContainerRepository < ActiveRecord::Base
belongs_to :project
validates :name, length: { minimum: 0, allow_nil: false }
validates :name, uniqueness: { scope: :project_id }
delegate :client, to: :registry
before_destroy :delete_tags!
def registry
@registry ||= begin
token = Auth::ContainerRegistryAuthenticationService.full_access_token(path)
url = Gitlab.config.registry.api_url
host_port = Gitlab.config.registry.host_port
ContainerRegistry::Registry.new(url, token: token, path: host_port)
end
end
def path
@path ||= [project.full_path, name].select(&:present?).join('/')
end
def tag(tag)
ContainerRegistry::Tag.new(self, tag)
end
def manifest
@manifest ||= client.repository_tags(path)
end
def tags
return @tags if defined?(@tags)
return [] unless manifest && manifest['tags']
@tags = manifest['tags'].map do |tag|
ContainerRegistry::Tag.new(self, tag)
end
end
def blob(config)
ContainerRegistry::Blob.new(self, config)
end
def has_tags?
tags.any?
end
def root_repository?
name.empty?
end
def delete_tags!
return unless has_tags?
digests = tags.map { |tag| tag.digest }.to_set
digests.all? do |digest|
client.delete_repository_tag(self.path, digest)
end
end
def self.build_from_path(path)
self.new(project: path.repository_project,
name: path.repository_name)
end
def self.create_from_path!(path)
build_from_path(path).tap(&:save!)
end
def self.build_root_repository(project)
self.new(project: project, name: '')
end
end
module EE
module Issue
# override
def check_for_spam?
author.support_bot? || super
end
# override
def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support
# bot should be a participant (which is what the superclass
# method checks for). However, the support bot gets filtered out
# at the end of Participable#raw_participants as not being able
# to read the project. Overriding *that* behavior is problematic
# because it doesn't use the Policy framework, and instead uses a
# custom-coded Ability.users_that_can_read_project, which is...
# a pain to override in EE. So... here we say, the support bot
# is subscribed by default, until an unsubscribed record appears,
# even though it's not *technically* a participant in this issue.
# Making the support bot subscribed to every issue is not as bad as it
# seems, though, since it isn't permitted to :receive_notifications,
# and doesn't actually show up in the participants list.
user.support_bot? || super
end
end
end
......@@ -14,6 +14,8 @@ module EE
delegate :actual_shared_runners_minutes_limit,
:shared_runners_minutes_used?, to: :namespace
before_validation :auto_refresh_service_desk_key
end
def shared_runners_available?
......@@ -23,5 +25,33 @@ module EE
def shared_runners_minutes_limit_enabled?
!public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled?
end
def service_desk_address
return nil unless service_desk_available?
refresh_service_desk_key! if service_desk_mail_key.blank?
from = "service_desk+#{service_desk_mail_key}"
::Gitlab::IncomingEmail.reply_address(from)
end
def refresh_service_desk_key!
return unless service_desk_available?
self.service_desk_mail_key = SentNotification.reply_key
end
private
def service_desk_available?
@service_desk_available ||=
EE::Gitlab::ServiceDesk.enabled? && service_desk_enabled?
end
def auto_refresh_service_desk_key
if service_desk_mail_key.blank? || service_desk_enabled_changed?
refresh_service_desk_key!
end
end
end
end
......@@ -17,6 +17,22 @@ module EE
validate :cannot_be_admin_and_auditor
end
module ClassMethods
def support_bot
email_pattern = "support%s@#{Settings.gitlab.host}"
unique_internal(where(support_bot: true), 'support-bot', email_pattern) do |u|
u.bio = 'The GitLab support bot used for Service Desk'
u.name = 'GitLab Support Bot'
end
end
# override
def internal_attributes
super + [:support_bot]
end
end
def cannot_be_admin_and_auditor
if admin? && auditor?
errors.add(:admin, "user cannot also be an Auditor.")
......
......@@ -60,6 +60,31 @@ class GeoNodeStatus
sync_percentage(lfs_objects_count, lfs_objects_synced_count)
end
def attachments_count
@attachments_count ||= Upload.count
end
def attachments_count=(value)
@attachments_count = value.to_i
end
def attachments_synced_count
@attachments_synced_count ||= begin
upload_ids = Upload.pluck(:id)
synced_ids = Geo::FileRegistry.where(file_type: [:attachment, :avatar, :file]).pluck(:file_id)
(synced_ids & upload_ids).length
end
end
def attachments_synced_count=(value)
@attachments_synced_count = value.to_i
end
def attachments_synced_in_percentage
sync_percentage(attachments_count, attachments_synced_count)
end
private
def sync_percentage(total, synced)
......
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
prepend EE::Issue
include InternalId
include Issuable
include Referable
......@@ -48,6 +50,8 @@ class Issue < ActiveRecord::Base
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
after_save :expire_etag_cache
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
......@@ -270,4 +274,13 @@ class Issue < ActiveRecord::Base
def publicly_visible?
project.public? && !confidential?
end
def expire_etag_cache
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
project.namespace,
project,
self
)
Gitlab::EtagCaching::Store.new.touch(key)
end
end
......@@ -465,7 +465,7 @@ class MergeRequest < ActiveRecord::Base
end
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!ProtectedBranch.protected?(source_project, source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
......
......@@ -151,7 +151,7 @@ class Namespace < ActiveRecord::Base
end
def any_project_has_container_registry_tags?
projects.any?(&:has_container_registry_tags?)
all_projects.any?(&:has_container_registry_tags?)
end
def send_update_instructions
......@@ -219,6 +219,12 @@ class Namespace < ActiveRecord::Base
@old_repository_storage_paths ||= repository_storage_paths
end
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
Project.inside_path(full_path)
end
private
def repository_storage_paths
......@@ -226,7 +232,7 @@ class Namespace < ActiveRecord::Base
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
end
end
......
......@@ -136,6 +136,7 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :protected_tags, dependent: :destroy
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
......@@ -164,6 +165,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :container_repositories, dependent: :destroy
has_many :commit_statuses, dependent: :destroy
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
......@@ -391,10 +393,15 @@ class Project < ActiveRecord::Base
end
def sort(method)
if method == 'storage_size_desc'
case method.to_s
when 'storage_size_desc'
# storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
reorder(last_activity_at: :desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
else
order_by(method)
end
......@@ -443,32 +450,15 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self)
end
def container_registry_path_with_namespace
path_with_namespace.downcase
end
def container_registry_repository
return unless Gitlab.config.registry.enabled
@container_registry_repository ||= begin
token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace)
url = Gitlab.config.registry.api_url
host_port = Gitlab.config.registry.host_port
registry = ContainerRegistry::Registry.new(url, token: token, path: host_port)
registry.repository(container_registry_path_with_namespace)
end
end
def container_registry_repository_url
def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}"
"#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}"
end
end
def has_container_registry_tags?
return unless container_registry_repository
container_registry_repository.tags.any?
container_repositories.to_a.any?(&:has_tags?) ||
has_root_container_repository_tags?
end
def commit(ref = 'HEAD')
......@@ -978,14 +968,6 @@ class Project < ActiveRecord::Base
@repo_exists = false
end
# Branches that are not _exactly_ matched by a protected branch.
def open_branches
exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name)
branch_names = repository.branches.map(&:name)
non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names))
repository.branches.reject { |branch| non_open_branch_names.include? branch.name }
end
def root_ref?(branch)
repository.root_ref == branch
end
......@@ -1005,16 +987,8 @@ class Project < ActiveRecord::Base
"#{Gitlab.config.build_gitlab_kerberos_url + Gitlab::Application.routes.url_helpers.namespace_project_path(self.namespace, self)}.git"
end
# Check if current branch name is marked as protected in the system
def protected_branch?(branch_name)
return true if empty_repo? && default_branch_protected?
@protected_branches ||= self.protected_branches.to_a
ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
end
def user_can_push_to_empty_repo?(user)
!default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
def forked?
......@@ -1035,10 +1009,10 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace)
if has_container_registry_tags?
Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present!"
# we currently doesn't support renaming repository if it contains tags in container registry
raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
# we currently doesn't support renaming repository if it contains images in container registry
raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
......@@ -1472,7 +1446,7 @@ class Project < ActiveRecord::Base
]
if container_registry_enabled?
variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true }
variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true }
end
variables
......@@ -1523,7 +1497,7 @@ class Project < ActiveRecord::Base
end
def repository_and_lfs_size
statistics.storage_size + statistics.lfs_objects_size
statistics.total_repository_size
end
def above_size_limit?
......@@ -1608,11 +1582,6 @@ class Project < ActiveRecord::Base
"projects/#{id}/pushes_since_gc"
end
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
# Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
......@@ -1645,4 +1614,15 @@ class Project < ActiveRecord::Base
Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
end
##
# This method is here because of support for legacy container repository
# which has exactly the same path like project does, but which might not be
# persisted in `container_repositories` table.
#
def has_root_container_repository_tags?
return false unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(self).has_tags?
end
end
class ProtectableDropdown
def initialize(project, ref_type)
@project = project
@ref_type = ref_type
end
# Tags/branches which are yet to be individually protected
def protectable_ref_names
@protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names
end
def hash
protectable_ref_names.map { |ref_name| { text: ref_name, id: ref_name, title: ref_name } }
end
private
def refs
@project.repository.public_send(@ref_type)
end
def ref_names
refs.map(&:name)
end
def protections
@project.public_send("protected_#{@ref_type}")
end
def non_wildcard_protected_ref_names
protections.reject(&:wildcard?).map(&:name)
end
end
class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter
belongs_to :project
validates :name, presence: true
validates :project, presence: true
include ProtectedRef
has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy
......@@ -30,38 +27,6 @@ class ProtectedBranch < ActiveRecord::Base
# 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)) }
def commit
project.commit(self.name)
end
# Returns all protected branches that match the given branch name.
# This realizes all records from the scope built up so far, and does
# _not_ return a relation.
#
# This method optionally takes in a list of `protected_branches` to search
# through, to avoid calling out to the database.
def self.matching(branch_name, protected_branches: nil)
(protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) }
end
# Returns all branches (among the given list of branches [`Gitlab::Git::Branch`])
# that match the current protected branch.
def matching(branches)
branches.select { |branch| self.matches?(branch.name) }
end
# Checks if the protected branch matches the given branch name.
def matches?(branch_name)
return false if self.name.blank?
exact_match?(branch_name) || wildcard_match?(branch_name)
end
# Checks if this protected branch contains a wildcard
def wildcard?
self.name && self.name.include?('*')
end
# 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
......@@ -80,22 +45,15 @@ class ProtectedBranch < ActiveRecord::Base
end
end
protected
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
def exact_match?(branch_name)
self.name == branch_name
self.matching(ref_name, protected_refs: project.protected_branches).present?
end
def wildcard_match?(branch_name)
wildcard_regex === branch_name
end
def wildcard_regex
@wildcard_regex ||= begin
name = self.name.gsub('*', 'STAR_DONT_ESCAPE')
quoted_name = Regexp.quote(name)
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
/\A#{regex_string}\z/
end
def self.default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
end
class ProtectedRefMatcher
def initialize(protected_ref)
@protected_ref = protected_ref
end
# Returns all protected refs that match the given ref name.
# This checks all records from the scope built up so far, and does
# _not_ return a relation.
#
# This method optionally takes in a list of `protected_refs` to search
# through, to avoid calling out to the database.
def self.matching(type, ref_name, protected_refs: nil)
(protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) }
end
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
# that match the current protected ref.
def matching(refs)
refs.select { |ref| @protected_ref.matches?(ref.name) }
end
# Checks if the protected ref matches the given ref name.
def matches?(ref_name)
return false if @protected_ref.name.blank?
exact_match?(ref_name) || wildcard_match?(ref_name)
end
# Checks if this protected ref contains a wildcard
def wildcard?
@protected_ref.name && @protected_ref.name.include?('*')
end
protected
def exact_match?(ref_name)
@protected_ref.name == ref_name
end
def wildcard_match?(ref_name)
return false unless wildcard?
wildcard_regex === ref_name
end
def wildcard_regex
@wildcard_regex ||= begin
name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE')
quoted_name = Regexp.quote(name)
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
/\A#{regex_string}\z/
end
end
end
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
def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present?
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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