Commit b8401cd0 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into bootstrap4

parents b75f9721 7f01d49b
......@@ -364,10 +364,11 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- retry gem install fog-aws mime-types
- retry gem install fog-aws mime-types activesupport
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
......@@ -735,16 +736,50 @@ codequality:
expire_in: 1 week
sast:
<<: *except-docs
image: registry.gitlab.com/gitlab-org/gl-sast:latest
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
CONFIDENCE_LEVEL: 2
SAST_CONFIDENCE_LEVEL: 2
DOCKER_DRIVER: overlay2
allow_failure: true
tags: []
before_script: []
cache: {}
dependencies: []
services:
- docker:stable-dind
script:
- /app/bin/run .
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
paths: [gl-sast-report.json]
dependency_scanning:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
tags: []
before_script: []
cache: {}
dependencies: []
services:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
paths: [gl-dependency-scanning-report.json]
qa:internal:
<<: *dedicated-no-docs-no-db-pull-cache-job
services: []
......
......@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
- Correct copy text for the promote milestone and label modals. !17726
- Avoid validation errors when running the Pages domain verification service. !17992
- Fix autolinking URLs containing ampersands. !18045
- Fix exceptions raised when migrating pipeline stages in the background. !18076
- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
- Don't show Jump to Discussion button on Issues.
- Fix listing commit branch/tags that contain special characters.
- Fix 404 in group boards when moving issue between lists.
### Performance (1 change)
- Free open file descriptors and libgit2 buffers in UpdatePagesService.
## 10.6.3 (2018-04-03)
### Security (2 changes)
......
......@@ -321,6 +321,9 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.7)
......@@ -878,7 +881,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
spinach (0.10.1)
spinach (0.8.10)
colorize
gherkin-ruby (>= 0.3.2)
json
......@@ -1072,6 +1075,7 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
......
......@@ -55,14 +55,13 @@
},
methods: {
successCallback(resp) {
return resp.json().then((response) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
const pipelines = resp.data.pipelines || resp.data;
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: {
pipelines: response,
pipelines: resp.data,
},
});
......@@ -70,7 +69,6 @@
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
});
},
},
};
......
......@@ -22,7 +22,7 @@ export default {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
return this.file && this.file.binary && !this.file.content;
},
editTabCSS() {
return {
......@@ -212,7 +212,7 @@ export default {
<content-viewer
v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw"
:path="file.rawPath"
:path="file.rawPath || file.path"
:file-size="file.size"
:project-path="file.projectId"/>
</div>
......
......@@ -1190,12 +1190,12 @@ export default class Notes {
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>';
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>';
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
......
......@@ -317,10 +317,10 @@ Please check your network connection and try again.`;
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget
issuable-type="issue"
v-else-if="!canCreateNote"
v-else-if="isLocked(getNoteableData) && !canCreateNote"
/>
<ul
v-else
v-else-if="canCreateNote"
class="notes notes-form timeline">
<li class="timeline-entry">
<div class="timeline-entry-inner">
......
......@@ -40,6 +40,10 @@ export default {
type: Boolean,
required: true,
},
canAwardEmoji: {
type: Boolean,
required: true,
},
canDelete: {
type: Boolean,
required: true,
......@@ -74,9 +78,6 @@ export default {
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
canAddAwardEmoji() {
return this.currentUserId;
},
isAuthoredByCurrentUser() {
return this.authorId === this.currentUserId;
},
......@@ -149,7 +150,7 @@ export default {
</button>
</div>
<div
v-if="canAddAwardEmoji"
v-if="canAwardEmoji"
class="note-actions-item">
<a
v-tooltip
......
......@@ -28,6 +28,10 @@ export default {
type: Number,
required: true,
},
canAwardEmoji: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters(['getUserData']),
......@@ -67,9 +71,6 @@ export default {
isAuthoredByMe() {
return this.noteAuthorId === this.getUserData.id;
},
isLoggedIn() {
return this.getUserData.id;
},
},
created() {
this.emojiSmiling = emojiSmiling;
......@@ -156,7 +157,7 @@ export default {
return title;
},
handleAward(awardName) {
if (!this.isLoggedIn) {
if (!this.canAwardEmoji) {
return;
}
......@@ -208,7 +209,7 @@ export default {
</span>
</button>
<div
v-if="isLoggedIn"
v-if="canAwardEmoji"
class="award-menu-holder">
<button
v-tooltip
......
......@@ -112,6 +112,7 @@ export default {
:note-author-id="note.author.id"
:awards="note.award_emoji"
:toggle-award-path="note.toggle_award_path"
:can-award-emoji="note.current_user.can_award_emoji"
/>
<note-attachment
v-if="note.attachment"
......
......@@ -258,7 +258,9 @@ Please check your network connection and try again.`;
:key="note.id"
/>
</ul>
<div class="discussion-reply-holder">
<div
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder">
<template v-if="!isReplying && canReply">
<div
class="btn-group d-flex discussion-with-resolve-btn"
......
......@@ -177,6 +177,7 @@ export default {
:note-id="note.id"
:access-level="note.human_access"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
......
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import ProjectNew from '../shared/project_new';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectLoadingSpinner();
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
......
import ProjectNew from '../shared/project_new';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectLoadingSpinner();
initProjectVisibilitySelector();
initProjectNew.bindEvents();
});
/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import $ from 'jquery';
import VisibilitySelect from '../../../visibility_select';
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
export default class ProjectNew {
constructor() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', () => {
$('.project-edit-container').hide();
return $('.save-project-loader').show();
});
this.initVisibilitySelect();
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
initVisibilitySelect() {
const visibilityContainer = document.querySelector('.js-visibility-select');
if (!visibilityContainer) return;
const visibilitySelect = new VisibilitySelect(visibilityContainer);
visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
let projectVisibility = $visibilitySelect.val();
const PROJECT_VISIBILITY_PRIVATE = '0';
$visibilitySelect.on('change', () => {
const newProjectVisibility = $visibilitySelect.val();
if (projectVisibility !== newProjectVisibility) {
this.$projectSelects.each((idx, select) => {
const $select = $(select);
const $options = $select.find('option');
const values = $.map($options, e => e.value);
// if switched to "private", limit visibility options
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
if ($select.val() !== values[0] && $select.val() !== values[1]) {
$select.val(values[1]).trigger('change');
highlightChanges($select);
}
$options.slice(2).disable();
}
// if switched from "private", increase visibility for non-disabled options
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
$options.enable();
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
$select.val(values[values.length - 1]).trigger('change');
highlightChanges($select);
}
}
});
projectVisibility = newProjectVisibility;
}
});
}
toggleSettings() {
this.$selects.each(function () {
var $select = $(this);
var className = $select.data('field')
.replace(/_/g, '-')
.replace('access-level', 'feature');
ProjectNew._showOrHide($select, '.' + className);
});
}
toggleSettingsOnclick() {
this.$selects.on('change', this.toggleSettings);
}
static _showOrHide(checkElement, container) {
const $container = $(container);
if ($(checkElement).val() !== '0') {
return $container.show();
}
return $container.hide();
}
toggleRepoVisibility() {
var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
.hide();
$repoAccessLevel
.off('change')
.on('change', function () {
var selectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.each(function () {
var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').enable();
if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal).trigger('change');
highlightChanges($this);
}
$this.find("option[value='" + selectedVal + "']").nextAll().disable();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
prevSelectedVal = selectedVal;
}.bind(this));
}
}
import $ from 'jquery';
export default function initProjectLoadingSpinner() {
const $formContainer = $('.project-edit-container');
const $loadingSpinner = $('.save-project-loader');
// show loading spinner when saving
$formContainer.on('ajax:before', () => {
$formContainer.hide();
$loadingSpinner.show();
});
}
......@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
* Does that setting the current selected tab in the localStorage
*/
export default class SigninTabsMemoizer {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.new-session-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
......
......@@ -7,10 +7,7 @@
import TablePagination from '../../vue_shared/components/table_pagination.vue';
import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
import {
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
......@@ -19,10 +16,7 @@
NavigationTabs,
NavigationControls,
},
mixins: [
pipelinesMixin,
CIPaginationMixin,
],
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
store: {
type: Object,
......@@ -147,25 +141,26 @@
*/
shouldRenderTabs() {
const { stateMap } = this.$options;
return this.hasMadeRequest &&
[
stateMap.loading,
stateMap.tableList,
stateMap.error,
stateMap.emptyTab,
].includes(this.stateToRender);
return (
this.hasMadeRequest &&
[stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
this.stateToRender,
)
);
},
shouldRenderButtons() {
return (this.newPipelinePath ||
this.resetCachePath ||
this.ciLintPath) && this.shouldRenderTabs;
return (
(this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
);
},
shouldRenderPagination() {
return !this.isLoading &&
return (
!this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
this.state.pageInfo.total > this.state.pageInfo.perPage
);
},
emptyTabMessage() {
......@@ -229,15 +224,13 @@
},
methods: {
successCallback(resp) {
return resp.json().then((response) => {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeCount(response.count);
if (_.isEqual(resp.config.params, this.requestData)) {
this.store.storeCount(resp.data.count);
this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines);
this.setCommonData(resp.data.pipelines);
}
});
},
/**
* Handles URL and query parameter changes.
......@@ -251,8 +244,9 @@
this.updateInternalState(parameters);
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
return this.service
.getPipelines(this.requestData)
.then(response => {
this.isLoading = false;
this.successCallback(response);
......@@ -271,13 +265,11 @@
handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true;
this.service.postAction(endpoint)
this.service
.postAction(endpoint)
.then(() => {
this.isResetCacheButtonLoading = false;
createFlash(
s__('Pipelines|Project cache successfully reset.'),
'notice',
);
createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
})
.catch(() => {
this.isResetCacheButtonLoading = false;
......
......@@ -13,16 +13,16 @@
* 3. Merge request widget
* 4. Commit widget
*/
import axios from '../../lib/utils/axios_utils';
import Flash from '../../flash';
import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
loadingIcon,
icon,
LoadingIcon,
Icon,
},
directives: {
......@@ -88,9 +88,8 @@
},
fetchJobs() {
this.$http.get(this.stage.dropdown_path)
.then(response => response.json())
.then((data) => {
axios.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
......@@ -98,8 +97,7 @@
this.closeDropdown();
this.isLoading = false;
const flash = new Flash('Something went wrong on our end.');
return flash;
Flash('Something went wrong on our end.');
});
},
......
......@@ -40,10 +40,8 @@ export default class pipelinesMediator {
}
successCallback(response) {
return response.json().then((data) => {
this.state.isLoading = false;
this.store.storePipeline(data);
});
this.store.storePipeline(response.data);
}
errorCallback() {
......
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class PipelineService {
constructor(endpoint) {
this.pipeline = Vue.resource(endpoint);
this.pipeline = endpoint;
}
getPipeline() {
return this.pipeline.get();
return axios.get(this.pipeline);
}
// eslint-disable-next-line
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`);
return axios.post(`${endpoint}.json`);
}
}
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class PipelinesService {
/**
* Commits and merge request endpoints need to be requested with `.json`.
*
......@@ -16,20 +10,18 @@ export default class PipelinesService {
* @param {String} root
*/
constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`;
this.endpoint = `${root}.json`;
} else {
endpoint = root;
this.endpoint = root;
}
this.pipelines = Vue.resource(endpoint);
}
getPipelines(data = {}) {
const { scope, page } = data;
return this.pipelines.get({ scope, page });
return axios.get(this.endpoint, {
params: { scope, page },
});
}
/**
......@@ -38,7 +30,8 @@ export default class PipelinesService {
* @param {String} endpoint
* @return {Promise}
*/
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`);
return axios.post(`${endpoint}.json`);
}
}
<script>
export default {
name: 'time-tracking-estimate-only-pane',
name: 'TimeTrackingEstimateOnlyPane',
props: {
timeEstimateHumanReadable: {
type: String,
required: true,
},
},
template: `
};
</script>
<template>
<div class="time-tracking-estimate-only-pane">
<span class="bold">
{{ s__('TimeTracking|Estimated:') }}
</span>
{{ timeEstimateHumanReadable }}
</div>
`,
};
</template>
<script>
import { sprintf, s__ } from '../../../locale';
export default {
name: 'time-tracking-help-state',
name: 'TimeTrackingHelpState',
props: {
rootPath: {
type: String,
......@@ -27,7 +28,10 @@ export default {
);
},
},
template: `
};
</script>
<template>
<div class="time-tracking-help-state">
<div class="time-tracking-info">
<h4>
......@@ -48,5 +52,4 @@ export default {
</a>
</div>
</div>
`,
};
</template>
<script>
import timeTrackingHelpState from './help_state';
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane';
import timeTrackingEstimateOnlyPane from './estimate_only_pane';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub';
......@@ -12,11 +12,11 @@ export default {
name: 'IssuableTimeTracker',
components: {
TimeTrackingCollapsedState,
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
TimeTrackingHelpState,
},
props: {
time_estimate: {
......
......@@ -7,7 +7,10 @@ export default {
statusIcon,
},
props: {
mr: { type: Object, required: true },
mr: {
type: Object,
required: true,
},
},
};
</script>
......@@ -20,13 +23,14 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
{{ s__("mrWidget|There are unresolved discussions. Please resolve these discussions") }}
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
class="btn btn-secondary btn-xs js-create-issue">
Create an issue to resolve them later
class="btn btn-secondary btn-xs js-create-issue"
>
{{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
</div>
</div>
......
......@@ -27,20 +27,22 @@
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
isMarkdownForm(form) {
return form && !form.find('.js-vue-markdown-field').length;
isValid(form) {
return !form ||
form.find('.js-vue-markdown-field').length ||
$(this.$el).closest('form') === form[0];
},
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
if (!this.isValid(form)) return;
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
if (!this.isValid(form)) return;
this.$emit('write-markdown');
},
......
......@@ -813,7 +813,6 @@
}
.discussion-notes {
padding: 0 $gl-padding $gl-padding;
min-height: 35px;
&:first-child {
......
......@@ -154,26 +154,10 @@
a {
width: 100%;
font-size: 18px;
margin-right: 0;
&:hover {
border: 1px solid transparent;
}
}
&.active {
border-bottom: 1px solid $border-color;
a {
border: 0;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
&:hover {
border-bottom: 2px solid $link-underline-blue;
}
}
&.active > a {
cursor: default;
}
}
}
......
......@@ -173,7 +173,11 @@
}
.discussion-form {
padding-top: $gl-padding-top;
background-color: $white-light;
}
.discussion-form-container {
padding: $gl-padding-top $gl-padding $gl-padding;
}
.discussion-notes .disabled-comment {
......@@ -233,7 +237,12 @@
.discussion-body,
.diff-file {
.discussion-reply-holder {
padding-top: $gl-padding;
background-color: $white-light;
padding: 10px 16px;
&.is-replying {
padding-bottom: $gl-padding;
}
}
}
......
......@@ -47,7 +47,7 @@ ul.notes {
}
.timeline-entry-inner {
padding: $gl-padding 0;
padding: $gl-padding $gl-btn-padding;
border-bottom: 1px solid $white-normal;
}
......@@ -94,6 +94,12 @@ ul.notes {
}
}
&.note-discussion {
.timeline-entry-inner {
padding: $gl-padding 10px;
}
}
.editing-spinner {
display: none;
}
......@@ -346,8 +352,6 @@ ul.notes {
}
.discussion-notes {
background-color: $white-light;
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
......@@ -359,6 +363,10 @@ ul.notes {
}
}
.notes {
background-color: $white-light;
}
a code {
top: 0;
margin-right: 0;
......@@ -639,6 +647,8 @@ ul.notes {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
border-bottom: 0;
}
}
......
......@@ -344,7 +344,6 @@
svg {
vertical-align: middle;
margin-right: 3px;
}
.stage-column {
......@@ -495,17 +494,12 @@
svg {
fill: $gl-text-color-secondary;
position: relative;
left: 1px;
top: -1px;
width: 16px;
height: 16px;
}
&.play {
svg {
width: 16px;
height: 16px;
left: 3px;
left: 2px;
}
}
}
......
......@@ -935,11 +935,6 @@ pre.light-well {
}
}
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
}
.flash-container {
padding: 0;
}
......
......@@ -56,7 +56,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
params[:application_setting] ||= {}
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
else
......
module ChecksCollaboration
def can_collaborate_with_project?(project, ref: nil)
return true if can?(current_user, :push_code, project)
can_create_merge_request =
can?(current_user, :create_merge_request_in, project) &&
current_user.already_forked?(project)
can_create_merge_request ||
user_access(project).can_push_to_branch?(ref)
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
# enabling this so we can easily cache the user access value as it might be
# used across multiple calls in the view
def user_access(project)
@user_access ||= {}
@user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
......@@ -41,7 +41,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note], @project)
Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
......@@ -56,7 +56,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note], @project)
Notes::RenderService.new(current_user).execute([@note])
end
respond_to do |format|
......
......@@ -4,7 +4,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
Notes::RenderService.new(current_user).execute(notes, @project)
Notes::RenderService.new(current_user).execute(notes)
notes
end
......
......@@ -173,7 +173,9 @@ class GroupsController < Groups::ApplicationController
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
Events::RenderService
.new(current_user)
.execute(@events, atom_request: request.format.atom?)
end
def user_actions
......
class Projects::ApplicationController < ApplicationController
include RoutableActions
include ChecksCollaboration
skip_before_action :authenticate_user!
before_action :project
......@@ -31,14 +32,6 @@ class Projects::ApplicationController < ApplicationController
@repository ||= project.repository
end
def can_collaborate_with_project?(project = nil, ref: nil)
project ||= @project
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) ||
user_access(project).can_push_to_branch?(ref)
end
def authorize_action!(action)
unless can?(current_user, action, project)
return access_denied!
......@@ -91,9 +84,4 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
def user_access(project)
@user_access ||= {}
@user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
end
end
......@@ -34,6 +34,7 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
@pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
respond_to do |format|
format.html
......
......@@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request]
before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
respond_to :html
......
......@@ -5,7 +5,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
skip_before_action :merge_request
before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_create_merge_request!
before_action :authorize_create_merge_request_from!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
......
......@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
private
def render_json_with_notes_serializer
Notes::RenderService.new(current_user).execute([note], project)
Notes::RenderService.new(current_user).execute([note])
render json: note_serializer.represent(note)
end
......
......@@ -159,7 +159,10 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
opts = { current_user: current_user }
opts[:project_ids_relation] = item_project_ids(items) if items
ProjectsFinder.new(opts).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
......@@ -316,9 +319,9 @@ class IssuableFinder
def by_project(items)
items =
if project?
items.of_projects(projects(items)).references_project
elsif projects(items)
items.merge(projects(items).reorder(nil)).join_project
items.of_projects(projects).references_project
elsif projects
items.merge(projects.reorder(nil)).join_project
else
items.none
end
......
......@@ -12,6 +12,7 @@ class MergeRequestTargetProjectFinder
if @source_project.fork_network
@source_project.fork_network.projects
.public_or_visible_to_user(current_user)
.non_archived
.with_feature_available_for_user(:merge_requests, current_user)
else
Project.where(id: source_project)
......
......@@ -59,7 +59,7 @@ module BlobHelper
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
......@@ -280,7 +280,7 @@ module BlobHelper
options << link_to("submit an issue", new_project_issue_path(project))
end
merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
merge_project = merge_request_source_project_for_project(@project)
if merge_project
options << link_to("create a merge request", project_new_merge_request_path(project))
end
......@@ -334,7 +334,7 @@ module BlobHelper
# Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
elsif current_user && can?(current_user, :fork_project, project)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
end
......
......@@ -94,7 +94,7 @@ module CiStatusHelper
def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
project = pipeline_status.project
path = pipelines_project_commit_path(project, pipeline_status.sha)
path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
render_status_with_link(
'commit',
......@@ -105,7 +105,7 @@ module CiStatusHelper
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
path = pipelines_project_commit_path(project, commit)
path = pipelines_project_commit_path(project, commit, ref: ref)
render_status_with_link(
'commit',
......
......@@ -163,7 +163,7 @@ module CommitsHelper
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
if can_collaborate_with_project?
if can_collaborate_with_project?(@project)
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
......
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
can?(current_user, :create_merge_request, project) &&
can?(current_user, :create_merge_request_from, project) &&
project.repository.branch_exists?(from) &&
project.repository.branch_exists?(to)
end
......
......@@ -82,8 +82,8 @@ module IssuesHelper
names.to_sentence
end
def award_state_class(awards, current_user)
if !current_user
def award_state_class(awardable, awards, current_user)
if !can?(current_user, :award_emoji, awardable)
"disabled"
elsif current_user && awards.find { |a| a.user_id == current_user.id }
"active"
......@@ -126,6 +126,17 @@ module IssuesHelper
link_to link_text, path
end
def show_new_issue_link?(project)
return false unless project
return false if project.archived?
# We want to show the link to users that are not signed in, that way they
# get directed to the sign-in/sign-up flow and afterwards to the new issue page.
return true unless current_user
can?(current_user, :create_issue, project)
end
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
......
......@@ -138,6 +138,18 @@ module MergeRequestsHelper
end
end
def merge_request_source_project_for_project(project = @project)
unless can?(current_user, :create_merge_request_in, project)
return nil
end
if can?(current_user, :create_merge_request_from, project)
project
else
current_user.fork_of(project)
end
end
def merge_params_ee(merge_request)
{}
end
......
......@@ -6,10 +6,6 @@ module NotesHelper
end
end
def note_editable?(note)
Ability.can_edit_note?(current_user, note)
end
def note_supports_quick_actions?(note)
Notes::QuickActionsService.supported?(note)
end
......
......@@ -157,40 +157,6 @@ module ProjectsHelper
current_user&.recent_push(@project)
end
def project_feature_access_select(field)
# Don't show option "everyone with access" if project is private
options = project_feature_options
level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
if @project.private?
disabled_option = ProjectFeature::ENABLED
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
options = options_for_select(
options.invert,
selected: highest_available_option || level,
disabled: disabled_option
)
content_tag :div, class: "select-wrapper" do
concat(
content_tag(
:select,
options,
name: "project[project_feature_attributes][#{field}]",
id: "project_project_feature_attributes_#{field}",
class: "pull-right form-control select-control #{repo_children_classes(field)} ",
data: { field: field }
)
)
concat(
icon('chevron-down')
)
end.html_safe
end
def link_to_autodeploy_doc
link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank'
end
......@@ -274,16 +240,6 @@ module ProjectsHelper
private
def repo_children_classes(field)
needs_repo_check = [:merge_requests_access_level, :builds_access_level]
return unless needs_repo_check.include?(field)
classes = "project-repo-select js-repo-select"
classes << " disabled" unless @project.feature_available?(:repository, current_user)
classes
end
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
......@@ -447,14 +403,6 @@ module ProjectsHelper
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
def project_feature_options
{
ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'),
ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'),
ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access')
}
end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
......@@ -463,20 +411,6 @@ module ProjectsHelper
IssuesFinder.new(current_user, project_id: project.id).execute
end
def visibility_select_options(project, selected_level)
level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
next if restricted_levels.include?(level)
level_options << [
visibility_level_label(level),
{ data: { description: visibility_level_description(level, project) } },
level
]
end
options_for_select(level_options, selected_level)
end
def restricted_levels
return [] if current_user.admin?
......
......@@ -46,10 +46,6 @@ class Ability
end
end
def can_edit_note?(user, note)
allowed?(user, :edit_note, note)
end
def allowed?(user, action, subject = :global, opts = {})
if subject.is_a?(Hash)
opts, subject = subject, :global
......
......@@ -19,7 +19,7 @@ class BroadcastMessage < ActiveRecord::Base
after_commit :flush_redis_cache
def self.current
messages = Rails.cache.fetch(CACHE_KEY) { current_and_future_messages.to_a }
messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) { current_and_future_messages.to_a }
return messages if messages.empty?
......@@ -36,6 +36,10 @@ class BroadcastMessage < ActiveRecord::Base
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
def self.cache_expires_in
nil
end
def active?
started? && !ended?
end
......
......@@ -79,11 +79,7 @@ module Awardable
end
def user_can_award?(current_user, name)
if user_authored?(current_user)
!awardable_votes?(normalize_name(name))
else
true
end
awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self)
end
def user_authored?(current_user)
......@@ -119,4 +115,12 @@ module Awardable
def normalize_name(name)
Gitlab::Emoji.normalize_emoji_name(name)
end
def awardable_by_user?(current_user, name)
if user_authored?(current_user)
!awardable_votes?(normalize_name(name))
else
true
end
end
end
......@@ -34,7 +34,7 @@ class DeployToken < ActiveRecord::Base
end
def has_access_to?(requested_project)
project == requested_project
active? && project == requested_project
end
# This is temporal. Currently we limit DeployToken
......
......@@ -110,7 +110,10 @@ class Event < ActiveRecord::Base
end
end
# Remove this method when removing Gitlab.rails5? code.
def subclass_from_attributes(attrs)
return super if Gitlab.rails5?
# Without this Rails will keep calling this method on the returned class,
# resulting in an infinite loop.
return unless self == Event
......
......@@ -11,7 +11,7 @@ module Ci
end
condition(:owner_of_job) do
can?(:developer_access) && @subject.triggered_by?(@user)
@subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
......@@ -19,6 +19,6 @@ module Ci
prevent :erase_build
end
rule { can?(:master_access) | owner_of_job }.enable :erase_build
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
end
end
......@@ -7,23 +7,17 @@ module Ci
end
condition(:owner_of_schedule) do
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
pipeline_schedule.owned_by?(@user)
end
condition(:non_owner_of_schedule) do
!pipeline_schedule.owned_by?(@user)
end
rule { can?(:developer_access) }.policy do
enable :play_pipeline_schedule
end
rule { can?(:create_pipeline) }.enable :play_pipeline_schedule
rule { can?(:master_access) | owner_of_schedule }.policy do
rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
enable :update_pipeline_schedule
enable :admin_pipeline_schedule
end
rule { can?(:master_access) & non_owner_of_schedule }.policy do
rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
......
......@@ -18,9 +18,7 @@ class IssuablePolicy < BasePolicy
rule { locked & ~is_project_member }.policy do
prevent :create_note
prevent :update_note
prevent :admin_note
prevent :resolve_note
prevent :edit_note
end
end
class NotePolicy < BasePolicy
delegate { @subject.project }
delegate { @subject.noteable if @subject.noteable.lockable? }
delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) }
condition(:is_author) { @user && @subject.author == @user }
condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
condition(:editable, scope: :subject) { @subject.editable? }
rule { ~editable | anonymous }.prevent :edit_note
rule { is_author | admin }.enable :edit_note
rule { can?(:master_access) }.enable :edit_note
rule { ~editable }.prevent :admin_note
rule { is_author }.policy do
enable :read_note
enable :update_note
enable :admin_note
enable :resolve_note
end
rule { for_merge_request & is_noteable_author }.policy do
rule { is_noteable_author }.policy do
enable :resolve_note
end
end
......@@ -25,4 +25,6 @@ class PersonalSnippetPolicy < BasePolicy
end
rule { anonymous }.prevent :comment_personal_snippet
rule { can?(:comment_personal_snippet) }.enable :award_emoji
end
class ProjectPolicy < BasePolicy
def self.create_read_update_admin(name)
[
:"create_#{name}",
:"read_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
extend ClassMethods
READONLY_FEATURES_WHEN_ARCHIVED = %i[
issue
list
merge_request
label
milestone
project_snippet
wiki
note
pipeline
pipeline_schedule
build
trigger
environment
deployment
commit_status
container_image
pages
cluster
].freeze
desc "User is a project owner"
condition :owner do
......@@ -15,7 +29,7 @@ class ProjectPolicy < BasePolicy
end
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject) { project.public_builds? }
condition(:public_builds, scope: :subject, score: 0) { project.public_builds? }
# For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
......@@ -35,7 +49,7 @@ class ProjectPolicy < BasePolicy
condition(:master) { team_access_level >= Gitlab::Access::MASTER }
desc "Project is public"
condition(:public_project, scope: :subject) { project.public? }
condition(:public_project, scope: :subject, score: 0) { project.public? }
desc "Project is visible to internal users"
condition(:internal_access) do
......@@ -46,7 +60,7 @@ class ProjectPolicy < BasePolicy
condition(:group_member, scope: :subject) { project_group_member? }
desc "Project is archived"
condition(:archived, scope: :subject) { project.archived? }
condition(:archived, scope: :subject, score: 0) { project.archived? }
condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? }
......@@ -56,10 +70,10 @@ class ProjectPolicy < BasePolicy
end
desc "Project has an external wiki"
condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? }
condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? }
desc "Project has request access enabled"
condition(:request_access_enabled, scope: :subject) { project.request_access_enabled }
condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
......@@ -126,6 +140,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:guest_access) }.policy do
enable :read_project
enable :create_merge_request_in
enable :read_board
enable :read_list
enable :read_wiki
......@@ -140,6 +155,7 @@ class ProjectPolicy < BasePolicy
enable :create_note
enable :upload_file
enable :read_cycle_analytics
enable :award_emoji
end
# These abilities are not allowed to admins that are not members of the project,
......@@ -197,7 +213,7 @@ class ProjectPolicy < BasePolicy
enable :create_pipeline
enable :update_pipeline
enable :create_pipeline_schedule
enable :create_merge_request
enable :create_merge_request_from
enable :create_wiki
enable :push_code
enable :resolve_note
......@@ -208,7 +224,7 @@ class ProjectPolicy < BasePolicy
end
rule { can?(:master_access) }.policy do
enable :delete_protected_branch
enable :push_to_delete_protected_branch
enable :update_project_snippet
enable :update_environment
enable :update_deployment
......@@ -231,37 +247,50 @@ class ProjectPolicy < BasePolicy
end
rule { archived }.policy do
prevent :create_merge_request
prevent :push_code
prevent :delete_protected_branch
prevent :update_merge_request
prevent :admin_merge_request
prevent :push_to_delete_protected_branch
prevent :request_access
prevent :upload_file
prevent :resolve_note
prevent :create_merge_request_from
prevent :create_merge_request_in
prevent :award_emoji
READONLY_FEATURES_WHEN_ARCHIVED.each do |feature|
prevent(*create_update_admin_destroy(feature))
end
end
rule { issues_disabled }.policy do
prevent(*create_read_update_admin_destroy(:issue))
end
rule { merge_requests_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin(:merge_request))
prevent :create_merge_request_in
prevent :create_merge_request_from
prevent(*create_read_update_admin_destroy(:merge_request))
end
rule { issues_disabled & merge_requests_disabled }.policy do
prevent(*create_read_update_admin(:label))
prevent(*create_read_update_admin(:milestone))
prevent(*create_read_update_admin_destroy(:label))
prevent(*create_read_update_admin_destroy(:milestone))
end
rule { snippets_disabled }.policy do
prevent(*create_read_update_admin(:project_snippet))
prevent(*create_read_update_admin_destroy(:project_snippet))
end
rule { wiki_disabled & ~has_external_wiki }.policy do
prevent(*create_read_update_admin(:wiki))
prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin(:build))
prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline]))
prevent(*create_read_update_admin(:pipeline_schedule))
prevent(*create_read_update_admin(:environment))
prevent(*create_read_update_admin(:deployment))
prevent(*create_update_admin_destroy(:pipeline))
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:deployment))
end
rule { repository_disabled }.policy do
......@@ -272,7 +301,7 @@ class ProjectPolicy < BasePolicy
end
rule { container_registry_disabled }.policy do
prevent(*create_read_update_admin(:container_image))
prevent(*create_read_update_admin_destroy(:container_image))
end
rule { anonymous & ~public_project }.prevent_all
......@@ -314,13 +343,6 @@ class ProjectPolicy < BasePolicy
enable :read_pipeline_schedule
end
rule { issues_disabled }.policy do
prevent :create_issue
prevent :update_issue
prevent :admin_issue
prevent :read_issue
end
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
......
class ProjectPolicy
module ClassMethods
def create_read_update_admin_destroy(name)
[
:"read_#{name}",
*create_update_admin_destroy(name)
]
end
def create_update_admin_destroy(name)
[
:"create_#{name}",
:"update_#{name}",
:"admin_#{name}",
:"destroy_#{name}"
]
end
end
end
......@@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include MarkupHelper
include TreeHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :merge_request
......@@ -152,11 +153,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def can_revert_on_current_merge_request?
user_can_collaborate_with_project? && cached_can_be_reverted?
can_collaborate_with_project?(project) && cached_can_be_reverted?
end
def can_cherry_pick_on_current_merge_request?
user_can_collaborate_with_project? && can_be_cherry_picked?
can_collaborate_with_project?(project) && can_be_cherry_picked?
end
def can_push_to_source_branch?
......@@ -195,12 +196,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence
end
def user_can_collaborate_with_project?
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) ||
can_push_to_source_branch?
end
def user_can_fork_project?
can?(current_user, :fork_project, project)
end
......
......@@ -29,6 +29,10 @@ class IssueEntity < IssuableEntity
expose :can_update do |issue|
can?(request.current_user, :update_issue, issue)
end
expose :can_award_emoji do |issue|
can?(request.current_user, :award_emoji, issue)
end
end
expose :create_note_path do |issue|
......
......@@ -15,7 +15,11 @@ class NoteEntity < API::Entities::Note
expose :current_user do
expose :can_edit do |note|
Ability.can_edit_note?(request.current_user, note)
Ability.allowed?(request.current_user, :admin_note, note)
end
expose :can_award_emoji do |note|
Ability.allowed?(request.current_user, :award_emoji, note)
end
end
......
......@@ -149,7 +149,8 @@ module Auth
def deploy_token_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
current_user.is_a?(DeployToken) &&
current_user.has_access_to?(requested_project)
current_user.has_access_to?(requested_project) &&
current_user.read_registry?
end
##
......
......@@ -4,9 +4,6 @@ module Ci
class RegisterJobService
attr_reader :runner
JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?)
def initialize(runner)
......@@ -107,22 +104,10 @@ module Ci
end
def register_success(job)
labels = { shared_runner: runner.shared?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
attempt_counter.increment
end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end
......@@ -132,7 +117,7 @@ module Ci
end
def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
end
end
end
module Events
class RenderService < BaseRenderer
def execute(events, atom_request: false)
events.map(&:note).compact.group_by(&:project).each do |project, notes|
render_notes(notes, project, atom_request)
end
notes = events.map(&:note).compact
render_notes(notes, atom_request)
end
private
def render_notes(notes, project, atom_request)
Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
def render_notes(notes, atom_request)
Notes::RenderService
.new(current_user)
.execute(notes, render_options(atom_request))
end
def render_options(atom_request)
......
......@@ -71,8 +71,8 @@ module MergeRequests
params.delete(:source_project_id)
params.delete(:target_project_id)
unless can?(current_user, :read_project, @source_project) &&
can?(current_user, :read_project, @project)
unless can?(current_user, :create_merge_request_from, @source_project) &&
can?(current_user, :create_merge_request_in, @project)
raise Gitlab::Access::AccessDeniedError
end
......
......@@ -3,19 +3,18 @@ module Notes
# Renders a collection of Note instances.
#
# notes - The notes to render.
# project - The project to use for redacting.
# user - The user viewing the notes.
#
# Possible options:
#
# requested_path - The request path.
# project_wiki - The project's wiki.
# ref - The current Git reference.
# only_path - flag to turn relative paths into absolute ones.
# xhtml - flag to save the html in XHTML
def execute(notes, project, **opts)
renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
renderer.render(notes, :note)
def execute(notes, options = {})
Banzai::ObjectRenderer
.new(user: current_user, redaction_context: options)
.render(notes, :note)
end
end
end
......@@ -101,7 +101,7 @@
- if @project.archived?
%li
%span.light archived:
%strong repository is read-only
%strong project is read-only
%li
%span.light access:
......
......@@ -33,7 +33,7 @@
= link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
= link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
- if can?(current_user, :destroy_user, user)
%li.divider
- if user.can_be_removed?
......
......@@ -3,13 +3,13 @@
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
class: [(award_state_class(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- if current_user
- if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add reaction',
......
%ul.nav-links.nav-tabs.new-session-tabs.single-tab
%ul.nav-links.new-session-tabs.single-tab
%li.active
%a= tab_title
%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
......
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%ul.nav-links.new-session-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
......
......@@ -19,8 +19,8 @@
%li.dropdown-bold-header GitLab
- if @project&.persisted?
- create_project_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- create_project_issue = show_new_issue_link?(@project)
- merge_project = merge_request_source_project_for_project(@project)
- create_project_snippet = can?(current_user, :create_project_snippet, @project)
- if create_project_issue || merge_project || create_project_snippet
%li.dropdown-bold-header This project
......
......@@ -13,7 +13,7 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
Overview
Project
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
......
......@@ -13,6 +13,7 @@
#{time_ago_with_tooltip(event.created_at)}
- if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
.flex-right
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
#{ _('Create merge request') }
- if can_change_visibility_level?(@project, current_user)
.select-wrapper
= form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control')
= icon('chevron-down')
- else
.info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
= visibility_level_icon(@project.visibility_level)
%strong
= visibility_level_label(@project.visibility_level)
......@@ -4,7 +4,7 @@
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- merge_project = merge_request_source_project_for_project(@project)
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
......@@ -61,7 +61,7 @@
title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
- elsif protected_branch?(@project, branch)
- if can?(current_user, :delete_protected_branch, @project)
- if can?(current_user, :push_to_delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
......
- if current_user
- can_create_issue = show_new_issue_link?(@project)
- can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
- can_push_code = can?(current_user, :push_code, @project)
- create_mr_from_new_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
- merge_project = merge_request_source_project_for_project(@project)
- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project
- if show_menu
.project-action-button.dropdown.inline
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- can_create_project_snippet = can?(current_user, :create_project_snippet, @project)
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
......@@ -20,17 +24,17 @@
- if can_create_project_snippet
%li= link_to _('New snippet'), new_project_snippet_path(@project)
- if can?(current_user, :push_code, @project)
- if can_push_code
%li.dropdown-header= _('This repository')
- if can?(current_user, :push_code, @project)
- if can_push_code
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- unless @project.empty_repo?
%li= link_to _('New branch'), new_project_branch_path(@project)
%li= link_to _('New tag'), new_project_tag_path(@project)
- elsif current_user && current_user.already_forked?(@project)
- elsif can_collaborate_with_project?(@project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- elsif can?(current_user, :fork_project, @project)
- elsif create_mr_from_new_fork
- continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
......
......@@ -7,5 +7,6 @@
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
- if can?(current_user, :create_cluster, @project)
.text-center
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
- can_collaborate = can_collaborate_with_project?(@project)
.page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
.header-main-content
= render partial: 'signature', object: @commit.signature
......@@ -32,12 +34,13 @@
%li.d-block.d-sm-none.d-md-none
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- unless @commit.has_been_reverted?(current_user)
- if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- if can_collaborate
%li.clearfix
= cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- if can_collaborate_with_project?
- if can?(current_user, :push_code, @project)
%li.clearfix
= link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit)
%li.divider
......
......@@ -17,6 +17,6 @@
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
- if can_collaborate_with_project?
- if can_collaborate_with_project?(@project)
- %w(revert cherry-pick).each do |type|
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title
......@@ -5,6 +5,7 @@
- link = commit_path(project, commit, merge_request: merge_request)
- cache_key = [project.full_path,
ref,
commit.id,
Gitlab::CurrentSettings.current_application_settings,
@path.presence,
......@@ -54,7 +55,7 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
.commit-sha-group
.label.label-monospace
......
......@@ -114,17 +114,18 @@
Archive project
- if @project.archived?
%p
Unarchiving the project will mark its repository as active. The project can be committed to.
Unarchiving the project will restore people's ability to make changes to it.
The repository can be committed to, and issues, comments and other entities can be created.
%strong Once active this project shows up in the search and on the dashboard.
= link_to 'Unarchive project', unarchive_project_path(@project),
data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
data: { confirm: "Are you sure that you want to unarchive this project?" },
method: :post, class: "btn btn-success"
- else
%p
Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
%strong Archived projects cannot be committed to!
Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches.
%strong The repository cannot be committed to, and no issues, comments or other entities can be created.
= link_to 'Archive project', archive_project_path(@project),
data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
data: { confirm: "Are you sure that you want to archive this project?" },
method: :post, class: "btn btn-warning"
.sub-section.rename-respository
%h4.warning-title
......
......@@ -2,7 +2,8 @@
= icon('rss')
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-secondary append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
- if show_new_issue_link?(@project)
= link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: finder.assignee.try(:id),
milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-new",
......
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- if can?(current_user, :push_code, @project)
- can_create_merge_request = can?(current_user, :create_merge_request_in, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
......
......@@ -7,6 +7,7 @@
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
.detail-page-header
.detail-page-header-body
......@@ -42,6 +43,7 @@
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
......@@ -50,6 +52,7 @@
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
......
......@@ -55,7 +55,7 @@
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.has_trace?
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden<
......
- @no_container = true
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- merge_project = merge_request_source_project_for_project(@project)
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
......
......@@ -36,7 +36,7 @@
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
- if current_user
- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
.note-actions-item
......
.card.protected-branches-list.js-protected-branches-list
.protected-branches-list.js-protected-branches-list
- if @protected_branches.empty?
.card-header
%h3.card-title
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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