Commit 23ec110e authored by Constance Okoghenun's avatar Constance Okoghenun

Resolved conflict with master

parents 1cb5f662 8da6ef70
Please view this file on the master branch, on stable branches it's out of date.
## 10.5.1 (2018-02-22)
- No changes.
## 10.5.0 (2018-02-22)
### Fixed (23 changes, 1 of them is from the community)
......@@ -68,6 +72,7 @@ Please view this file on the master branch, on stable branches it's out of date.
### Other (3 changes)
- Activated the Web IDE Button also on the main project page. !4250
- Geo - add documentation about using shared a S3 bucket with GitLab Container Registry.
- Geo: Improve replication status. Using pg_stat_wal_receiver.
- Remove unaproved typo check in sast:container report.
......
......@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.5.1 (2018-02-22)
- No changes.
## 10.5.0 (2018-02-22)
### Security (3 changes, 1 of them is from the community)
......@@ -165,9 +169,8 @@ entry.
- Log and send a system hook if a blocked user attempts to login.
- Add Gitaly Servers admin dashboard.
### Other (26 changes, 7 of them are from the community)
### Other (25 changes, 7 of them are from the community)
- Activated the Web IDE Button also on the main project page. !4250
- Updated the katex library. !15864
- Add modal for deleting a milestone. !16229
- Remove unused CSS selectors for Cycle Analytics. !16270 (Takuya Noguchi)
......
10.5.0-pre
10.6.0-pre
/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
/* global ResolveCount */
/* global ResolveServiceClass */
import Vue from 'vue';
import './models/discussion';
......
This diff is collapsed.
......@@ -14,7 +14,6 @@ export default class DropdownUser extends FilteredSearchDropdown {
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search',
params: {
per_page: 20,
active: true,
group_id: this.getGroupId(),
project_id: this.getProjectId(),
......
......@@ -51,16 +51,6 @@ export default class FilteredSearchDropdownManager {
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-author'),
},
label: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json${this.groupsOnly ? '?only_group_labels=true' : ''}`,
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
element: this.container.querySelector('#js-dropdown-label'),
},
assignee: {
reference: null,
gl: DropdownUser,
......@@ -75,6 +65,16 @@ export default class FilteredSearchDropdownManager {
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json${this.groupsOnly ? '?only_group_labels=true' : ''}`,
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
element: this.container.querySelector('#js-dropdown-label'),
},
'my-reaction': {
reference: null,
gl: DropdownEmoji,
......
......@@ -634,7 +634,7 @@ GitLabDropdown = (function() {
html.style.display = 'none';
}
if ((data === 'divider' || data === 'separator')) {
if (data === 'divider' || data === 'separator') {
html.className = data;
return html;
}
......
......@@ -123,9 +123,10 @@ export default {
and cause odd behavior when one is removed.
-->
<li
:key="`${pendingReferences.length}-${reference}`"
v-for="(reference, index) in pendingReferences"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item">
:key="`related-issues-token-${index}`"
class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item"
>
<issue-token
event-namespace="pendingIssuable"
:id-key="index"
......
......@@ -316,7 +316,7 @@ export default class LabelsSelect {
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
clicked: function (clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
......
......@@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => {
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
};
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -10,7 +10,7 @@ window.jQuery = jQuery;
window.$ = jQuery;
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
......@@ -107,13 +107,7 @@ document.addEventListener('DOMContentLoaded', () => {
return true;
});
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$('.js-select-on-focus').on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
$(this).tooltip('destroy')
......
......@@ -33,4 +33,3 @@
<div v-html="sanitizedOutput"></div>
</div>
</template>
......@@ -325,8 +325,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
<div class="note-form-actions">
<div
class="pull-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
@click.prevent="handleSave()"
:disabled="isSubmitButtonDisabled"
......
......@@ -30,8 +30,7 @@
rel="noopener noreferrer">
<i
class="fa fa-paperclip"
aria-hidden="true"
>
aria-hidden="true">
</i>
{{ attachment.filename }}
</a>
......
......@@ -116,8 +116,7 @@
<template>
<div
ref="editNoteForm"
class="note-edit-form current-note-edit-form"
>
class="note-edit-form current-note-edit-form">
<div
v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger">
......
import UserCallout from '../../../../user_callout';
import UserCallout from '~/user_callout';
export default () => new UserCallout();
document.addEventListener('DOMContentLoaded', () => new UserCallout());
import DueDateSelectors from '../../../due_date_select';
import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors();
document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import CILintEditor from './ci_lint_editor';
export default () => new CILintEditor();
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import initGroupsList from '~/groups';
export default initGroupsList;
document.addEventListener('DOMContentLoaded', initGroupsList);
import Labels from '~/labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels';
export default initLabels;
document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
......@@ -9,4 +9,4 @@ export default () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
};
});
import UsersSelect from '../../../../users_select';
import UsersSelect from '~/users_select';
export default () => new UsersSelect();
document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import DueDateSelectors from '../../../due_date_select';
import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors();
document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import BuildArtifacts from '~/build_artifacts';
import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () {
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
new BuildArtifacts(); // eslint-disable-line no-new
}
});
import BlobViewer from '~/blob/viewer/index';
import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () {
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
}
});
import ProjectFork from '~/project_fork';
export default () => {
new ProjectFork(); // eslint-disable-line no-new
};
document.addEventListener('DOMContentLoaded', () => new ProjectFork());
......@@ -3,6 +3,7 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new
......
import Labels from '~/labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels';
export default initLabels;
document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
......@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () {
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
......@@ -22,4 +22,4 @@ export default function () {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
}
});
import initSettingsPanels from '~/settings_panels';
export default initSettingsPanels;
document.addEventListener('DOMContentLoaded', initSettingsPanels);
......@@ -12,6 +12,7 @@
projectFeatureToggle,
projectSettingRow,
},
props: {
currentSettings: {
type: Object,
......@@ -95,6 +96,7 @@
return visibilityLevelDescriptions[this.visibilityLevel];
},
},
watch: {
visibilityLevel(value, oldValue) {
if (value === visibilityOptions.PRIVATE) {
......@@ -151,6 +153,7 @@
else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
},
methods: {
highlightChanges() {
this.highlightChangesClass = true;
......
......@@ -106,7 +106,7 @@ export default class ProjectNew {
}
}
toggleRepoVisibility () {
toggleRepoVisibility() {
var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
......@@ -117,7 +117,8 @@ export default class ProjectNew {
.nextAll()
.hide();
$repoAccessLevel.off('change')
$repoAccessLevel
.off('change')
.on('change', function () {
var selectedVal = parseInt($repoAccessLevel.val(), 10);
......
import Search from './search';
export default () => new Search();
document.addEventListener('DOMContentLoaded', () => new Search());
<script>
/* eslint-disable no-alert, vue/require-default-prop */
/* eslint-disable no-alert */
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......@@ -10,7 +10,6 @@
directives: {
tooltip,
},
components: {
loadingIcon,
icon,
......@@ -32,12 +31,15 @@
type: String,
required: true,
},
id: {
pipelineId: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
isLoading: false,
......@@ -48,17 +50,27 @@
return `btn ${this.cssClass}`;
},
},
created() {
// We're using eventHub to listen to the modal here instead of
// using props because it would would make the parent components
// much more complex to keep track of the loading state of each button
eventHub.$on('postAction', this.setLoading);
},
beforeDestroy() {
eventHub.$off('postAction', this.setLoading);
},
methods: {
onClick() {
eventHub.$emit('actionConfirmationModal', {
id: this.id,
callback: this.makeRequest,
eventHub.$emit('openConfirmationModal', {
pipelineId: this.pipelineId,
endpoint: this.endpoint,
type: this.type,
});
},
makeRequest() {
this.isLoading = true;
eventHub.$emit('postAction', this.endpoint);
setLoading(endpoint) {
if (endpoint === this.endpoint) {
this.isLoading = true;
}
},
},
};
......
......@@ -62,7 +62,8 @@
:class="{
'left-connector': index === 0 && (!isFirstColumn || hasTriggeredBy)
}"
:id="jobId(job)">
:id="jobId(job)"
>
<div class="curve"></div>
......
......@@ -19,7 +19,6 @@
required: true,
},
},
data() {
return {
actions: this.getActions(),
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue';
import retryConfirmationModal from './retry_confirmation_modal.vue';
import eventHub from '../event_hub';
/**
* Pipelines Table Component.
......@@ -11,8 +12,7 @@
export default {
components: {
pipelinesTableRowComponent,
stopConfirmationModal,
retryConfirmationModal,
modal,
},
props: {
pipelines: {
......@@ -33,6 +33,52 @@
required: true,
},
},
data() {
return {
pipelineId: '',
endpoint: '',
type: '',
};
},
computed: {
modalTitle() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false) :
sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false);
},
modalText() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false) :
sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false);
},
primaryButtonLabel() {
return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('openConfirmationModal', this.setModalData);
},
beforeDestroy() {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
},
},
};
</script>
<template>
......@@ -74,7 +120,20 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
<stop-confirmation-modal />
<retry-confirmation-modal />
<modal
id="confirmation-modal"
:title="modalTitle"
:text="modalText"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</div>
</template>
......@@ -306,9 +306,10 @@
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat"
:id="pipeline.id"
:pipeline-id="pipeline.id"
data-toggle="modal"
data-target="#retry-confirmation-modal"
data-target="#confirmation-modal"
type="retry"
/>
<async-button-component
......@@ -317,9 +318,10 @@
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
icon="close"
:id="pipeline.id"
:pipeline-id="pipeline.id"
data-toggle="modal"
data-target="#stop-confirmation-modal"
data-target="#confirmation-modal"
type="stop"
/>
</div>
</div>
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Retry pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to retry pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="retry-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Stop pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to stop pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Stop pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="stop-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
......@@ -85,7 +85,7 @@ export default class ProjectFindFile {
.catch(() => flash(__('An error occurred while loading filenames')));
}
// render result
// render result
renderList(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
......
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl) => {
......@@ -36,6 +38,7 @@ const bindEvents = () => {
const $changeTemplateBtn = $('.change-template');
const $selectedIcon = $('.selected-icon svg');
const $templateProjectNameInput = $('#template-project-name #project_path');
const $pushNewProjectTipTrigger = $('.push-new-project-tip');
if ($newProjectForm.length !== 1) {
return;
......@@ -55,6 +58,34 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'auto bottom',
html: 'true',
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
$(document).on('click.popover touchstart.popover', (event) => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
addSelectOnFocusBehaviour(target);
target.focus();
})
.on('hide.bs.popover', () => {
$(document).off('click.popover touchstart.popover');
});
}
function chooseTemplate() {
$('.template-option').hide();
$projectFieldsForm.addClass('selected');
......
......@@ -12,6 +12,7 @@
mixins: [
issuableMixin,
],
props: {
isLocked: {
required: true,
......@@ -31,6 +32,7 @@
},
},
},
computed: {
lockIcon() {
return this.isLocked ? 'lock' : 'lock-open';
......
......@@ -20,11 +20,9 @@ export default {
store: new Store(),
};
},
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
......
......@@ -39,7 +39,6 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter');
options.perPage = $dropdown.data('perPage');
showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove');
......@@ -669,7 +668,6 @@ UsersSelect.prototype.users = function(query, options, callback) {
const url = this.buildUrl(this.usersPath);
const params = {
search: query,
per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
......
......@@ -80,6 +80,7 @@
>
<ci-icon :status="status" />
</a>
<div class="media-body">
Pipeline
<a
......
......@@ -5,7 +5,6 @@
components: {
icon,
},
props: {
isLocked: {
type: Boolean,
......
......@@ -18,7 +18,7 @@
<template>
<button
type="button"
class="btn btn-blank gutter-toggle"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
>
<i
......
......@@ -444,6 +444,19 @@
}
}
.btn-missing {
color: $notes-light-color;
border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
&:hover,
&:active,
&:focus {
color: $notes-light-color;
background-color: $white-normal;
}
}
.btn-svg svg {
@include btn-svg;
}
......
......@@ -63,10 +63,6 @@
}
}
.project-stats {
display: none;
}
.group-buttons {
display: none;
}
......
......@@ -3,7 +3,6 @@
transition: padding $sidebar-transition-duration;
.container-fluid {
background: $white-light;
padding: 0 $gl-padding;
&.container-blank {
......
......@@ -296,7 +296,7 @@ body {
line-height: 1.3;
font-size: 1.25em;
font-weight: $gl-font-weight-bold;
margin: 12px 7px;
margin: 12px 0;
}
h1,
......@@ -333,6 +333,10 @@ a > code {
font-family: $monospace_font;
}
.weight-normal {
font-weight: $gl-font-weight-normal;
}
.commit-sha,
.ref-name {
@extend .monospace;
......
......@@ -216,8 +216,8 @@ $tooltip-font-size: 12px;
*/
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-padding-4: 4px;
$gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
......@@ -384,6 +384,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08);
$btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
$btn-white-active: #848484;
$gl-btn-padding: 10px;
$gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
/*
* Badges
......
......@@ -685,6 +685,9 @@ a.deploy-project-label {
}
}
.project-empty-note-panel {
border-bottom: 1px solid $border-color;
}
.project-stats {
font-size: 0;
......@@ -693,11 +696,13 @@ a.deploy-project-label {
border-bottom: 1px solid $border-color;
.nav {
padding-top: 12px;
padding-bottom: 12px;
margin-top: $gl-padding-8;
margin-bottom: $gl-padding-8;
> li {
display: inline-block;
margin-top: $gl-padding-4;
margin-bottom: $gl-padding-4;
&:not(:last-child) {
margin-right: $gl-padding;
......@@ -711,36 +716,32 @@ a.deploy-project-label {
float: right;
}
}
}
> a {
padding: 0;
background-color: transparent;
font-size: 14px;
line-height: 29px;
color: $notes-light-color;
.stat-text,
.stat-link {
padding: $gl-btn-vert-padding 0;
background-color: transparent;
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $notes-light-color;
}
&:hover,
&:focus {
color: $gl-text-color;
text-decoration: underline;
}
.stat-link {
&:hover,
&:focus {
color: $gl-text-color;
text-decoration: underline;
}
}
}
li.missing {
border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
a {
padding-left: 10px;
padding-right: 10px;
color: $notes-light-color;
display: block;
.btn {
padding: $gl-btn-vert-padding $gl-btn-horz-padding;
line-height: $gl-btn-line-height;
}
&:hover {
background-color: $gray-normal;
.btn-missing {
@extend .btn-missing;
}
}
}
......@@ -750,7 +751,7 @@ pre.light-well {
}
.git-empty {
margin: 0 7px 7px;
margin-bottom: 7px;
h5 {
color: $gl-text-color;
......@@ -907,6 +908,12 @@ a.allowed-to-push {
}
}
.project-tip-command {
> .input-group-btn:first-child {
width: auto;
}
}
.protected-branches-list,
.protected-tags-list {
margin-bottom: 30px;
......
......@@ -134,10 +134,15 @@ class ApplicationController < ActionController::Base
Ability.allowed?(object, action, subject)
end
def access_denied!
def access_denied!(message = nil)
respond_to do |format|
format.json { head :not_found }
format.any { render "errors/access_denied", layout: "errors", status: 404 }
format.any { head :not_found }
format.html do
render "errors/access_denied",
layout: "errors",
status: 404,
locals: { message: message }
end
end
end
......
......@@ -57,7 +57,7 @@ module Boards
end
def issue
@issue ||= issues_finder.execute.find(params[:id])
@issue ||= issues_finder.find(params[:id])
end
def filter_params
......
module ControllerWithCrossProjectAccessCheck
extend ActiveSupport::Concern
included do
extend Gitlab::CrossProjectAccess::ClassMethods
before_action :cross_project_check
end
def cross_project_check
if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
authorize_cross_project_page!
end
end
def authorize_cross_project_page!
return if can?(current_user, :read_cross_project)
rejection_message = _(
"This page is unavailable because you are not allowed to read information "\
"across multiple projects."
)
access_denied!(rejection_message)
end
end
......@@ -3,16 +3,20 @@ module RoutableActions
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
route_not_found
handle_not_found_or_authorized(routable)
nil
end
end
# This is overridden in gitlab-ee.
def handle_not_found_or_authorized(_routable)
route_not_found
end
def routable_authorized?(routable, extra_authorization_proc)
action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable)
......
......@@ -24,7 +24,7 @@ module UploadsActions
# - or redirect to its URL
#
def show
return render_404 unless uploader.exists?
return render_404 unless uploader&.exists?
if uploader.file_storage?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
......@@ -71,6 +71,9 @@ module UploadsActions
def build_uploader_from_params
uploader = uploader_class.new(model, secret: params[:secret])
return nil unless uploader.model_valid?
uploader.retrieve_from_store!(params[:filename])
uploader
end
......
class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
layout 'dashboard'
requires_cross_project_access
private
def projects
......
class Dashboard::GroupsController < Dashboard::ApplicationController
include GroupTree
skip_cross_project_access_check :index
def index
groups = GroupsFinder.new(current_user, all_available: false).execute
render_group_tree(groups)
......
......@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
before_action :set_non_archived_param
before_action :default_sorting
skip_cross_project_access_check :index, :starred
def index
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
......
class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
def index
@snippets = SnippetsFinder.new(
current_user,
......
class Groups::ApplicationController < ApplicationController
include RoutableActions
prepend EE::Groups::ApplicationController
include RoutableActions
include ControllerWithCrossProjectAccessCheck
layout 'group'
skip_before_action :authenticate_user!
before_action :group
requires_cross_project_access
private
......
class Groups::AvatarsController < Groups::ApplicationController
before_action :authorize_admin_group!
skip_cross_project_access_check :destroy
def destroy
@group.remove_avatar!
@group.save
......
module Groups
class ChildrenController < Groups::ApplicationController
before_action :group
skip_cross_project_access_check :index
def index
parent = if params[:parent_id].present?
......
......@@ -9,6 +9,14 @@ class Groups::GroupMembersController < Groups::ApplicationController
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access, :update, :override]
before_action :authorize_update_group_member!, only: [:update, :override]
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
def index
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
......
module Groups
module Settings
class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
before_action :authorize_admin_pipeline!
def show
......
......@@ -2,6 +2,8 @@ module Groups
class VariablesController < Groups::ApplicationController
before_action :authorize_admin_build!
skip_cross_project_access_check :show, :update
def show
respond_to do |format|
format.json do
......
......@@ -20,6 +20,12 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show, :subgroups]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
# project information
skip_cross_project_access_check :show, if: -> { request.format.html? }
layout :determine_layout
def index
......
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper
include Gitlab::Allowable
include PageLayoutHelper
include OauthApplications
......@@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
before_action :add_gon_variables
before_action :load_scopes, only: [:index, :create, :edit]
helper_method :can?
layout 'profile'
def index
......
class Projects::ApplicationController < ApplicationController
prepend EE::Projects::ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
......
......@@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
def target
case params[:type]&.downcase
when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'commit'
@project.commit(params[:type_id])
end
......
......@@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @branch_name == @ref
diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}"
......
......@@ -77,7 +77,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def branch_to
@target_project = selected_target_project
if params[:ref].present?
if @target_project && params[:ref].present?
@ref = params[:ref]
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end
......@@ -87,7 +87,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def update_branches
@target_project = selected_target_project
@target_branches = @target_project.repository.branch_names
@target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false
end
......@@ -123,7 +123,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project
elsif params[:target_project_id].present?
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
.execute.find(params[:target_project_id])
.find_by(id: params[:target_project_id])
else
@project.forked_from_project
end
......
......@@ -47,7 +47,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
render 'new'
render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
end
end
......@@ -117,6 +117,8 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user
@project = @project.present(current_user: current_user)
render_landing_page
end
......
class SearchController < ApplicationController
skip_before_action :authenticate_user!
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include RendersCommits
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
search_term_present && !params[:project_id].present?
end
layout 'search'
def show
......
class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access show: false,
groups: false,
projects: false,
contributed: false,
snippets: true,
calendar: false,
calendar_activities: true
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
......@@ -103,12 +112,7 @@ class UsersController < ApplicationController
end
def load_events
# Get user activity feed for projects common for both users
@events = user.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
@events = UserRecentEventsFinder.new(current_user, user, params).execute
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
......@@ -141,10 +145,6 @@ class UsersController < ApplicationController
).execute.page(params[:page])
end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
def build_canonical_path(user)
url_for(params.merge(username: user.to_param))
end
......
class AutocompleteUsersFinder
# The number of users to display in the results is hardcoded to 20, and
# pagination is not supported. This ensures that performance remains
# consistent and removes the need for implementing keyset pagination to ensure
# good performance.
LIMIT = 20
attr_reader :current_user, :project, :group, :search, :skip_users,
:page, :per_page, :author_id, :params
:author_id, :params
# EE
attr_reader :skip_ldap
......@@ -11,8 +17,6 @@ class AutocompleteUsersFinder
@group = group
@search = params[:search]
@skip_users = params[:skip_users]
@page = params[:page]
@per_page = params[:per_page]
@author_id = params[:author_id]
@params = params
......@@ -30,9 +34,10 @@ class AutocompleteUsersFinder
items = items.reorder(:name)
items = items.search(search) if search.present?
items = items.where.not(id: skip_users) if skip_users.present?
items = items.limit(LIMIT)
# EE
items = load_users_by_push_ability(items) || items.page(page).per(per_page)
items = load_users_by_push_ability(items)
if params[:todo_filter].present? && current_user
items = items.todo_authors(current_user.id, params[:todo_state_filter])
......@@ -64,22 +69,25 @@ class AutocompleteUsersFinder
end
def users_from_project
user_ids = project.team.users.pluck(:id)
user_ids << author_id if author_id.present?
if author_id.present?
union = Gitlab::SQL::Union
.new([project.authorized_users, User.where(id: author_id)])
User.where(id: user_ids)
User.from("(#{union.to_sql}) #{User.table_name}")
else
project.authorized_users
end
end
# EE
def load_users_by_push_ability(items)
return unless project
return items unless project
ability = push_ability
return if ability.blank?
return items if ability.blank?
items.to_a
.select { |user| user.can?(ability, project) }
.take(per_page&.to_i || Kaminari.config.default_per_page)
end
def push_ability
......
module FinderMethods
def find_by!(*args)
raise_not_found_unless_authorized execute.find_by!(*args)
end
def find_by(*args)
if_authorized execute.find_by(*args)
end
def find(*args)
raise_not_found_unless_authorized model.find(*args)
end
private
def raise_not_found_unless_authorized(result)
result = if_authorized(result)
raise ActiveRecord::RecordNotFound.new("Couldn't find #{model}") unless result
result
end
def if_authorized(result)
# Return the result if the finder does not perform authorization checks.
# this is currently the case in the `MilestoneFinder`
return result unless respond_to?(:current_user)
if can_read_object?(result)
result
else
nil
end
end
def can_read_object?(object)
# When there's no policy, we'll allow the read, this is for example the case
# for Todos
return true unless DeclarativePolicy.has_policy?(object)
model_name = object&.model_name || model.model_name
Ability.allowed?(current_user, :"read_#{model_name.singular}", object)
end
# This fetches the model from the `ActiveRecord::Relation` but does not
# actually execute the query.
def model
execute.model
end
end
# Module to prepend into finders to specify wether or not the finder requires
# cross project access
#
# This module depends on the finder implementing the following methods:
#
# - `#execute` should return an `ActiveRecord::Relation`
# - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
extend Gitlab::CrossProjectAccess::ClassMethods
end
override :execute
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
return original unless check
return original if should_skip_cross_project_check || can_read_cross_project?
if check.should_run?(self)
original.model.none
else
original
end
end
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
# itself.
override :find_by!
def find_by!(*args)
skip_cross_project_check { super }
end
override :find_by
def find_by(*args)
skip_cross_project_check { super }
end
override :find
def find(*args)
skip_cross_project_check { super }
end
private
attr_accessor :should_skip_cross_project_check
def skip_cross_project_check
self.should_skip_cross_project_check = true
yield
ensure
# The find could raise an `ActiveRecord::RecordNotFound`, after which we
# still want to re-enable the check.
self.should_skip_cross_project_check = false
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
def can_read_project?(project)
Ability.allowed?(current_user, :read_project, project)
end
end
class EventsFinder
prepend FinderMethods
prepend FinderWithCrossProjectAccess
attr_reader :source, :params, :current_user
requires_cross_project_access unless: -> { source.is_a?(Project) }
# Used to filter Events
#
# Arguments:
......
......@@ -21,8 +21,12 @@
# my_reaction_emoji: string
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include CreatedAtFilter
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze
attr_accessor :current_user, :params
......@@ -87,14 +91,6 @@ class IssuableFinder
by_my_reaction_emoji(items)
end
def find(*params)
execute.find(*params)
end
def find_by(*params)
execute.find_by(*params)
end
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
......@@ -124,10 +120,6 @@ class IssuableFinder
counts.with_indifferent_access
end
def find_by!(*params)
execute.find_by!(*params)
end
def group
return @group if defined?(@group)
......
class LabelsFinder < UnionFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
def initialize(current_user, params = {})
@current_user = current_user
@params = params
......
class MergeRequestTargetProjectFinder
include FinderMethods
attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:)
......
......@@ -8,6 +8,8 @@
# state - filters by state.
class MilestonesFinder
include FinderMethods
attr_reader :params, :project_ids, :group_ids
def initialize(params = {})
......
......@@ -13,7 +13,9 @@
# params are optional
class SnippetsFinder < UnionFinder
include Gitlab::Allowable
attr_accessor :current_user, :params, :project
include FinderMethods
attr_accessor :current_user, :project, :params
def initialize(current_user, params = {})
@current_user = current_user
......@@ -52,10 +54,14 @@ class SnippetsFinder < UnionFinder
end
def authorized_snippets
Snippet.where(feature_available_projects.or(not_project_related)).public_or_visible_to_user(current_user)
Snippet.where(feature_available_projects.or(not_project_related))
.public_or_visible_to_user(current_user)
end
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
......
......@@ -13,6 +13,11 @@
#
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze
attr_accessor :current_user, :params
......
# Get user activity feed for projects common for a user and a logged in user
#
# - current_user: The user viewing the events
# - user: The user for which to load the events
# - params:
# - offset: The page of events to return
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access
attr_reader :current_user, :target_user, :params
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
target_user
.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
end
......@@ -34,7 +34,7 @@ module ApplicationHelper
def project_icon(project_id, options = {})
project =
if project_id.is_a?(Project)
if project_id.respond_to?(:avatar_url)
project_id
else
Project.find_by_full_path(project_id)
......
......@@ -12,12 +12,6 @@ module BranchesHelper
project_branches_path(@project, @id, options)
end
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_exists?(branch_name)
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name)
end
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
......
......@@ -6,4 +6,28 @@ module DashboardHelper
def assigned_mrs_dashboard_path
merge_requests_dashboard_path(assignee_id: current_user.id)
end
def dashboard_nav_links
@dashboard_nav_links ||= get_dashboard_nav_links
end
def dashboard_nav_link?(link)
dashboard_nav_links.include?(link)
end
def any_dashboard_nav_link?(links)
links.any? { |link| dashboard_nav_link?(link) }
end
private
def get_dashboard_nav_links
links = [:projects, :groups, :snippets]
if can?(current_user, :read_cross_project)
links += [:activity, :milestones]
end
links
end
end
......@@ -25,8 +25,24 @@ module ExploreHelper
controller.class.name.split("::").first == "Explore"
end
def explore_nav_links
@explore_nav_links ||= get_explore_nav_links
end
def explore_nav_link?(link)
explore_nav_links.include?(link)
end
def any_explore_nav_link?(links)
links.any? { |link| explore_nav_link?(link) }
end
private
def get_explore_nav_links
[:projects, :groups, :snippets]
end
def request_path_with_options(options = {})
request.path + "?#{options.to_param}"
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.
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