Commit e69fe059 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-10-05

parents 62360c3b 40fa4c96
......@@ -28,7 +28,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
......@@ -93,12 +93,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
- export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
- '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
......@@ -240,7 +241,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
- mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
......@@ -259,22 +260,21 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- 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/${CI_PROJECT_NAME}/all_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -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
- rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
services: []
before_script: []
cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
only:
......@@ -288,7 +288,7 @@ flaky-examples-check:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
- scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
......
......@@ -17,7 +17,8 @@ class Diff {
}
});
FilesCommentButton.init($diffFile);
const tab = document.getElementById('diffs');
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file));
......
......@@ -55,7 +55,6 @@ import UserCallout from './user_callout';
import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import GeoNodeForm from './geo/geo_node_form';
import GeoNodes from './geo_nodes';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
......@@ -640,7 +639,9 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'geo_nodes':
new GeoNodes($('.geo-nodes'));
new GeoNodeForm($('.js-geo-node-form'));
import(/* webpackChunkName: 'geo_node_form' */ './geo/geo_node_form')
.then(geoNodeForm => geoNodeForm.default($('.js-geo-node-form')))
.catch(() => {});
break;
}
break;
......
export default class GeoNodeForm {
constructor(container) {
this.$container = container;
this.$namespaces = this.$container.find('.js-hide-if-geo-primary');
this.$namespacesSelect = this.$namespaces.find('.select2');
this.$primaryCheckbox = this.$container.find("input[type='checkbox']");
this.$primaryCheckbox.on('change', () => this.onPrimaryCheckboxChange());
}
onPrimaryCheckboxChange() {
this.$namespacesSelect.select2('data', null);
this.$namespaces.toggleClass('hidden', this.$primaryCheckbox.is(':checked'));
}
/* global Flash */
import {
s__,
} from '../locale';
import '../flash';
import Api from '../api';
const onPrimaryCheckboxChange = function onPrimaryCheckboxChange(e, $namespaces) {
const $namespacesSelect = $('.select2', $namespaces);
$namespacesSelect.select2('data', null);
$namespaces.toggleClass('hidden', e.currentTarget.checked);
};
export default function geoNodeForm($container) {
const $namespaces = $('.js-hide-if-geo-primary', $container);
const $primaryCheckbox = $('input[type="checkbox"]', $container);
const $select2Dropdown = $('.js-geo-node-namespaces', $container);
$primaryCheckbox.on('change', e => onPrimaryCheckboxChange(e, $namespaces));
$select2Dropdown.select2({
placeholder: s__('Geo|Select groups to replicate.'),
multiple: true,
initSelection($el, callback) {
callback($el.data('selected'));
},
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'JSON',
quietMillis: 250,
data(search) {
return {
search,
};
},
results(data) {
return {
results: data.map(group => ({
id: group.id,
text: group.full_name,
})),
};
},
},
});
}
......@@ -7,10 +7,12 @@
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueCommentForm',
......@@ -26,8 +28,9 @@
};
},
components: {
confidentialIssue,
issueWarning,
issueNoteSignedOutWidget,
issueDiscussionLockedWidget,
markdownField,
userAvatarLink,
},
......@@ -55,6 +58,9 @@
isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
canCreateNote() {
return this.getIssueData.current_user.can_create_note;
},
issueActionButtonTitle() {
if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen';
......@@ -90,9 +96,6 @@
endpoint() {
return this.getIssueData.create_note_path;
},
isConfidentialIssue() {
return this.getIssueData.confidential;
},
},
methods: {
...mapActions([
......@@ -220,6 +223,9 @@
});
},
},
mixins: [
issuableStateMixin,
],
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
......@@ -235,6 +241,7 @@
<template>
<div>
<issue-note-signed-out-widget v-if="!isLoggedIn" />
<issue-discussion-locked-widget v-else-if="!canCreateNote" />
<ul
v-else
class="notes notes-form timeline">
......@@ -253,15 +260,22 @@
<div class="timeline-content timeline-content-form">
<form
ref="commentForm"
class="new-note js-quick-submit common-note-form gfm-form js-main-target-form">
<confidentialIssue v-if="isConfidentialIssue" />
class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
>
<div class="error-alert"></div>
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
:is-confidential-issue="isConfidentialIssue"
ref="markdownField">
<textarea
id="note-body"
......
<script>
export default {
computed: {
lockIcon() {
return gl.utils.spriteIcon('lock');
},
},
};
</script>
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning">
<span class="icon" v-html="lockIcon"></span>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueNoteForm',
......@@ -39,12 +40,13 @@
};
},
components: {
confidentialIssue,
issueWarning,
markdownField,
},
computed: {
...mapGetters([
'getDiscussionLastNote',
'getIssueData',
'getIssueDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
......@@ -67,9 +69,6 @@
isDisabled() {
return !this.note.length || this.isSubmitting;
},
isConfidentialIssue() {
return this.getIssueDataByProp('confidential');
},
},
methods: {
handleUpdate() {
......@@ -95,6 +94,9 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
},
},
mixins: [
issuableStateMixin,
],
mounted() {
this.$refs.textarea.focus();
},
......@@ -125,7 +127,13 @@
<div class="flash-container timeline-content"></div>
<form
class="edit-note common-note-form js-quick-submit gfm-form">
<confidentialIssue v-if="isConfidentialIssue" />
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
......
export default {
methods: {
isConfidential(issue) {
return !!issue.confidential;
},
isLocked(issue) {
return !!issue.discussion_locked;
},
hasWarning(issue) {
return this.isConfidential(issue) || this.isLocked(issue);
},
},
};
......@@ -47,9 +47,9 @@ export default {
</script>
<template>
<div class="block confidentiality">
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
<i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
<i class="fa" :class="faEye" aria-hidden="true"></i>
</div>
<div class="title hide-collapsed">
Confidentiality
......@@ -62,19 +62,19 @@ export default {
Edit
</a>
</div>
<div class="value confidential-value hide-collapsed">
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
<i class="fa fa-eye sidebar-item-icon"></i>
Not confidential
</div>
<div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
<div v-else class="value sidebar-item-value hide-collapsed">
<i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
This issue is confidential
</div>
</div>
......
......@@ -2,9 +2,6 @@
import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
editFormButtons,
},
props: {
isConfidential: {
required: true,
......@@ -19,12 +16,16 @@ export default {
type: Function,
},
},
components: {
editFormButtons,
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu confidential-warning-message">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
......
......@@ -15,7 +15,7 @@ export default {
},
},
computed: {
onOrOff() {
toggleButtonText() {
return this.isConfidential ? 'Turn Off' : 'Turn On';
},
updateConfidentialBool() {
......@@ -26,7 +26,7 @@ export default {
</script>
<template>
<div class="confidential-warning-message-actions">
<div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
......@@ -39,7 +39,7 @@ export default {
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
>
{{ onOrOff }}
{{ toggleButtonText }}
</button>
</div>
</template>
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editFormButtons,
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message">
<p class="text" v-if="isLocked">
Unlock this {{ issuableDisplayName(issuableType) }}?
<strong>Everyone</strong>
will be able to comment.
</p>
<p class="text" v-else>
Lock this {{ issuableDisplayName(issuableType) }}?
Only
<strong>project members</strong>
will be able to comment.
</p>
<edit-form-buttons
:is-locked="isLocked"
:toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
</div>
</template>
<script>
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
},
computed: {
buttonText() {
return this.isLocked ? this.__('Unlock') : this.__('Lock');
},
toggleLock() {
return !this.isLocked;
},
},
};
</script>
<template>
<div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
@click.prevent="updateLockedAttribute(toggleLock)"
>
{{ buttonText }}
</button>
</div>
</template>
<script>
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editForm,
},
computed: {
lockIconClass() {
return this.isLocked ? 'fa-lock' : 'fa-unlock';
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
},
methods: {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
updateLockedAttribute(locked) {
this.mediator.service.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
<i
class="fa"
:class="lockIconClass"
aria-hidden="true"
></i>
</div>
<div class="title hide-collapsed">
Lock {{issuableDisplayName(issuableType) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
type="button"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</button>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
:toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
/>
<div
v-if="isLocked"
class="value sidebar-item-value"
>
<i
aria-hidden="true"
class="fa fa-lock sidebar-item-icon is-active"
></i>
{{ __('Locked') }}
</div>
<div
v-else
class="no-value sidebar-item-value hide-collapsed"
>
<i
aria-hidden="true"
class="fa fa-unlock sidebar-item-icon"
></i>
{{ __('Unlocked') }}
</div>
</div>
</div>
</template>
import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
Vue.use(Translate);
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
propsData: {
isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(el);
}
function mountLockComponent(mediator) {
const el = document.getElementById('js-lock-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const LockComp = Vue.extend(LockIssueSidebar);
new LockComp({
propsData: {
isLocked: initialData.is_locked,
isEditable: initialData.is_editable,
mediator,
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}).$mount(el);
}
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
const confidentialEl = document.querySelector('#js-confidential-entry-point');
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
if (confidentialEl) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
const ConfidentialComp = Vue.extend(confidential);
new ConfidentialComp({
propsData: {
isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(confidentialEl);
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
}
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
......
......@@ -15,6 +15,7 @@ export default class SidebarStore {
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
SidebarStore.singleton = this;
}
......
<script>
export default {
name: 'confidentialIssueWarning',
};
</script>
<template>
<div class="confidential-issue-warning">
<i
aria-hidden="true"
class="fa fa-eye-slash">
</i>
<span>
This is a confidential issue. Your comment will not be visible to the public.
</span>
</div>
</template>
<script>
export default {
props: {
isLocked: {
type: Boolean,
default: false,
required: false,
},
isConfidential: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
iconClass() {
return {
'fa-eye-slash': this.isConfidential,
'fa-lock': this.isLocked,
};
},
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
},
};
</script>
<template>
<div class="issuable-note-warning">
<i
aria-hidden="true"
class="fa"
:class="iconClass"
v-if="!isLockedAndConfidential"
></i>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
{{ __('People without permission will never get a notification and won\'t be able to comment.') }}
</span>
<span v-else-if="isConfidential">
{{ __('This is a confidential issue.') }}
{{ __('Your comment will not be visible to the public.') }}
</span>
<span v-else-if="isLocked">
{{ __('This issue is locked.') }}
{{ __('Only project members can comment.') }}
</span>
</div>
</template>
export default {
methods: {
issuableDisplayName(issuableType) {
const displayName = issuableType.replace(/_/, ' ');
return this.__ ? this.__(displayName) : displayName;
},
},
};
......@@ -385,7 +385,11 @@
background: transparent;
border: 0;
&:hover,
&:active,
&:focus {
outline: 0;
background: transparent;
box-shadow: none;
}
}
......@@ -722,3 +722,8 @@ Project Templates Icons
$rails: #c00;
$node: #353535;
$java: #70ad51;
/*
Issuable warning
*/
$issuable-warning-size: 24px;
......@@ -5,27 +5,25 @@
margin-right: auto;
}
.is-confidential {
.issuable-warning-icon {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
}
.is-not-confidential {
.sidebar-item-icon {
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
.confidentiality {
.is-not-confidential {
margin: auto;
}
.is-confidential {
margin: auto;
&.is-active {
color: $orange-600;
background-color: $orange-50;
}
}
......
......@@ -101,7 +101,7 @@
}
}
.confidential-issue-warning {
.issuable-note-warning {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
......@@ -112,26 +112,46 @@
align-items: center;
}
.confidential-value {
.disabled-comment .issuable-note-warning {
border: none;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
}
.sidebar-item-value {
.fa {
background-color: inherit;
}
}
.confidential-warning-message {
.sidebar-item-warning-message {
line-height: 1.5;
padding: 16px;
.confidential-warning-message-actions {
.text {
color: $text-color;
}
.sidebar-item-warning-message-actions {
display: flex;
button {
.btn {
flex-grow: 1;
}
}
}
.confidential-issue-warning + .md-area {
.issuable-note-warning + .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
......
......@@ -703,6 +703,12 @@ ul.notes {
color: $note-disabled-comment-color;
padding: 90px 0;
&.discussion-locked {
border: none;
background-color: $white-light;
}
a {
color: $gl-link-color;
}
......
......@@ -280,6 +280,7 @@ class Projects::IssuesController < Projects::ApplicationController
state_event
task_num
lock_version
discussion_locked
] + [{ label_ids: [], assignee_ids: [] }]
end
......
......@@ -36,6 +36,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:target_project_id,
:task_num,
:title,
:discussion_locked,
label_ids: []
]
......
......@@ -66,7 +66,16 @@ class Projects::NotesController < Projects::ApplicationController
params.merge(last_fetched_at: last_fetched_at)
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
def authorize_create_note!
return unless noteable.lockable?
access_denied! unless can?(current_user, :create_note, noteable)
end
end
......@@ -130,8 +130,12 @@ module NotesHelper
end
def can_create_note?
issuable = @issue || @merge_request
if @snippet.is_a?(PersonalSnippet)
can?(current_user, :comment_personal_snippet, @snippet)
elsif issuable
can?(current_user, :create_note, issuable)
else
can?(current_user, :create_note, @project)
end
......
......@@ -23,7 +23,9 @@ module SystemNoteHelper
'approved' => 'approval',
'unapproved' => 'unapproval',
'relate' => 'link',
'unrelate' => 'unlink'
'unrelate' => 'unlink',
'locked' => 'lock',
'unlocked' => 'lock-open'
}.freeze
def system_note_icon_name(note)
......
......@@ -74,4 +74,8 @@ module Noteable
def discussions_can_be_resolved_by?(user)
discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) }
end
def lockable?
[MergeRequest, Issue].include?(self.class)
end
end
......@@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
opened closed merged duplicate
opened closed merged duplicate locked unlocked
outdated
approved unapproved relate unrelate
].freeze
......
class IssuablePolicy < BasePolicy
delegate { @subject.project }
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
desc "User is the assignee or author"
condition(:assignee_or_author) do
@user && @subject.assignee_or_author?(@user)
......@@ -12,4 +16,12 @@ class IssuablePolicy < BasePolicy
enable :read_merge_request
enable :update_merge_request
end
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? }
condition(:is_author) { @user && @subject.author == @user }
condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? }
......@@ -8,6 +9,7 @@ class NotePolicy < BasePolicy
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
......
......@@ -3,6 +3,7 @@ class IssueEntity < IssuableEntity
expose :branch_name
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
expose :due_date
expose :moved_to_id
......@@ -17,7 +18,7 @@ class IssueEntity < IssuableEntity
expose :current_user do
expose :can_create_note do |issue|
can?(request.current_user, :create_note, issue.project)
can?(request.current_user, :create_note, issue)
end
expose :can_update do |issue|
......
module Geo
class NodeCreateService
attr_reader :params
def initialize(params)
@params = params.dup
@params[:namespace_ids] = @params[:namespace_ids].to_s.split(',')
end
def execute
GeoNode.create(params).persisted?
end
end
end
......@@ -6,6 +6,7 @@ module Geo
@geo_node = geo_node
@old_namespace_ids = geo_node.namespace_ids
@params = params.slice(:url, :primary, :namespace_ids)
@params[:namespace_ids] = @params[:namespace_ids].to_s.split(',')
end
def execute
......@@ -15,7 +16,7 @@ module Geo
Geo::RepositoriesChangedEventStore.new(geo_node).create
end
geo_node
true
end
private
......
......@@ -45,6 +45,10 @@ class IssuableBaseService < BaseService
SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
end
def create_discussion_lock_note(issuable)
SystemNoteService.discussion_lock(issuable, current_user)
end
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
......@@ -59,6 +63,7 @@ class IssuableBaseService < BaseService
params.delete(:due_date)
params.delete(:canonical_issue_id)
params.delete(:project)
params.delete(:discussion_locked)
end
filter_assignee(issuable)
......@@ -238,6 +243,7 @@ class IssuableBaseService < BaseService
handle_common_system_notes(issuable, old_labels: old_labels)
end
change_discussion_lock(issuable)
handle_changes(
issuable,
old_labels: old_labels,
......@@ -296,6 +302,12 @@ class IssuableBaseService < BaseService
end
end
def change_discussion_lock(issuable)
if issuable.previous_changes.include?('discussion_locked')
create_discussion_lock_note(issuable)
end
end
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
......
......@@ -34,7 +34,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user)
todo_service.reassigned_issue(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
......
......@@ -17,14 +17,14 @@ module MergeRequests
end
run_git_command(
%W(clone -b #{merge_request.source_branch} -- #{source_project.repository.path_to_repo} #{tree_path}),
nil,
%W(worktree add --detach #{tree_path} #{merge_request.source_branch}),
repository.path_to_repo,
git_env,
'clone repository for rebase'
'add worktree for rebase'
)
run_git_command(
%W(pull --rebase #{target_project.repository.path_to_repo} #{merge_request.target_branch}),
%W(rebase #{merge_request.target_branch}),
tree_path,
git_env.merge('GIT_COMMITTER_NAME' => current_user.name,
'GIT_COMMITTER_EMAIL' => current_user.email),
......@@ -32,20 +32,16 @@ module MergeRequests
)
rebase_sha = run_git_command(
%W(rev-parse #{merge_request.source_branch}),
%w(rev-parse HEAD),
tree_path,
git_env,
'get SHA of rebased branch'
)
merge_request.update_attributes(rebase_commit_sha: rebase_sha)
Gitlab::Git::OperationService.new(current_user, project.repository.raw_repository)
.update_branch(merge_request.source_branch, rebase_sha, merge_request.source_branch_sha)
run_git_command(
%W(push -f origin #{merge_request.source_branch}),
tree_path,
git_env,
'push rebased branch'
)
merge_request.update_attributes(rebase_commit_sha: rebase_sha)
true
rescue GitCommandError
......@@ -58,6 +54,8 @@ module MergeRequests
clean_dir
end
private
def tree_path
@tree_path ||= merge_request.rebase_dir_path
end
......
......@@ -645,6 +645,13 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
end
def discussion_lock(issuable, author)
action = issuable.discussion_locked? ? 'locked' : 'unlocked'
body = "#{action} this issue"
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
private
def notes_for_mentioner(mentioner, noteable, notes)
......
......@@ -43,8 +43,8 @@ class TodoService
#
# * create a pending todo for new assignee if issue is assigned
#
def reassigned_issue(issue, current_user)
create_assignment_todo(issue, current_user)
def reassigned_issue(issue, current_user, old_assignees = [])
create_assignment_todo(issue, current_user, old_assignees)
end
# When create a merge request we should:
......@@ -267,10 +267,11 @@ class TodoService
create_mention_todos(project, target, author, note, skip_users)
end
def create_assignment_todo(issuable, author)
def create_assignment_todo(issuable, author, old_assignees = [])
if issuable.assignees.any?
assignees = issuable.assignees - old_assignees
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignees, attributes)
create_todos(assignees, attributes)
end
end
......
......@@ -24,8 +24,8 @@
= link_to 'here', help_page_path('gitlab-geo/configuration.html', anchor: 'step-5-enabling-the-secondary-gitlab-node')
.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :namespace_ids, 'Namespaces to replicate', class: 'control-label'
= form.label :namespace_ids, s_('Geo|Groups to replicate'), class: 'control-label'
.col-sm-10
= form.select :namespace_ids, namespaces_options(geo_node.namespace_ids), { include_hidden: true }, multiple: true, class: 'select2 select-wide', data: { field: 'namespace_ids' }
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.help-block
Choose which namespaces you wish to replicate to this secondary node. Leave blank to replicate all.
#{ s_("Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all.") }
- referenced_users = local_assigns.fetch(:referenced_users, nil)
- if defined?(@merge_request) && @merge_request.discussion_locked?
.issuable-note-warning
= icon('lock')
%span
= _('This merge request is locked.')
= _('Only project members can comment.')
.md-area
.md-header
%ul.nav-links.clearfix
......
......@@ -30,7 +30,9 @@
.issuable-meta
- if @issue.confidential
= icon('eye-slash', class: 'is-confidential')
= icon('eye-slash', class: 'issuable-warning-icon')
- if @issue.discussion_locked?
= icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
......
......@@ -15,6 +15,8 @@
= icon('angle-double-left')
.issuable-meta
- if @merge_request.discussion_locked?
= icon('lock', class: 'issuable-warning-icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
......
......@@ -91,7 +91,7 @@
#pipelines.pipelines.tab-pane
- if @pipelines.any?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
#diffs.diffs.tab-pane
#diffs.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked? } }
-# This tab is always loaded via AJAX
.mr-loading-status
......
......@@ -147,6 +147,10 @@
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked)
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- subscribed = issuable.subscribed?(current_user, @project)
......
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
%ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes"
......@@ -21,5 +24,14 @@
or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
to comment
- elsif discussion_locked
.disabled-comment.text-center.prepend-top-default
%span.issuable-note-warning
%span.icon= sprite_icon('lock', size: 14)
%span
This
= issuable.class.to_s.titleize.downcase
is locked. Only
%b project members
can comment.
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
......@@ -18,23 +18,44 @@ module Geo
def find_object_ids
downloaded_ids = find_downloaded_ids([:attachment, :avatar, :file])
current_node.uploads
.where.not(id: downloaded_ids)
.order(created_at: :desc)
.limit(db_retrieve_batch_size)
.pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').downcase] }
unsynched_downloads = filter_downloaded_ids(
current_node.uploads, downloaded_ids, Upload.table_name)
unsynched_downloads
.order(created_at: :desc)
.limit(db_retrieve_batch_size)
.pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').downcase] }
end
def find_lfs_object_ids
downloaded_ids = find_downloaded_ids([:lfs])
current_node.lfs_objects
.where.not(id: downloaded_ids)
.order(created_at: :desc)
.limit(db_retrieve_batch_size)
.pluck(:id)
.map { |id| [id, :lfs] }
unsynched_downloads = filter_downloaded_ids(
current_node.lfs_objects, downloaded_ids, LfsObject.table_name)
unsynched_downloads
.order(created_at: :desc)
.limit(db_retrieve_batch_size)
.pluck(:id)
.map { |id| [id, :lfs] }
end
# This query requires data from two different databases, and unavoidably
# plucks a list of file IDs from one into the other. This will not scale
# well with the number of synchronized files--the query will increase
# linearly in size--so this should be replaced with postgres_fdw ASAP.
def filter_downloaded_ids(objects, downloaded_ids, table_name)
return objects if downloaded_ids.empty?
joined_relation = objects.joins(<<~SQL)
LEFT OUTER JOIN
(VALUES #{downloaded_ids.map { |id| "(#{id}, 't')" }.join(',')})
file_registry(file_id, registry_present)
ON #{table_name}.id = file_registry.file_id
SQL
joined_relation.where(file_registry: { registry_present: [nil, false] })
end
def find_downloaded_ids(file_types)
......
---
title: Geo - Selective replication allows admins to select any groups
merge_request: 2779
author:
type: fixed
---
title: Don't create todos for old issue assignees
merge_request:
author:
type: fixed
---
title: Improve performance of rebasing by using worktree
merge_request:
author:
type: changed
---
title: Save Geo files to a temporary file and rename after success
merge_request:
author:
type: fixed
---
title: 'Geo: Limit the huge cross-database pluck for LFS objects and attachments'
merge_request:
author:
type: fixed
title: Discussion lock for issues and merge requests
merge_request:
author:
type: added
class AddDiscussionLockedToIssuable < ActiveRecord::Migration
DOWNTIME = false
def up
add_column(:merge_requests, :discussion_locked, :boolean)
add_column(:issues, :discussion_locked, :boolean)
end
def down
remove_column(:merge_requests, :discussion_locked)
remove_column(:issues, :discussion_locked)
end
end
......@@ -871,6 +871,7 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
......@@ -1116,6 +1117,7 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.integer "head_pipeline_id"
t.boolean "ref_fetched"
t.string "merge_jid"
t.boolean "discussion_locked"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
......@@ -112,7 +112,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"confidential": false
"confidential": false,
"discussion_locked": false
}
]
```
......@@ -220,7 +221,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"confidential": false
"confidential": false,
"discussion_locked": false
}
]
```
......@@ -329,7 +331,8 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
"confidential": false
"confidential": false,
"discussion_locked": false
}
]
```
......@@ -414,6 +417,7 @@ Example response:
},
"confidential": false,
"weight": null,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
......@@ -491,6 +495,7 @@ Example response:
},
"confidential": false,
"weight": null,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
......@@ -525,6 +530,7 @@ PUT /projects/:id/issues/:issue_iid
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `weight` | integer | no | The weight of the issue in range 0 to 9 |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
......@@ -569,6 +575,7 @@ Example response:
},
"confidential": false,
"weight": null,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
......@@ -669,6 +676,7 @@ Example response:
},
"confidential": false,
"weight": null,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
......@@ -748,6 +756,7 @@ Example response:
},
"confidential": false,
"weight": null,
"discussion_locked": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
"notes": "http://example.com/api/v4/projects/1/issues/2/notes",
......@@ -778,6 +787,44 @@ POST /projects/:id/issues/:issue_iid/unsubscribe
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
```
Example response:
```json
{
"id": 93,
"iid": 12,
"project_id": 5,
"title": "Incidunt et rerum ea expedita iure quibusdam.",
"description": "Et cumque architecto sed aut ipsam.",
"state": "opened",
"created_at": "2016-04-05T21:41:45.217Z",
"updated_at": "2016-04-07T13:02:37.905Z",
"labels": [],
"milestone": null,
"assignee": {
"name": "Edwardo Grady",
"username": "keyon",
"id": 21,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/keyon"
},
"author": {
"name": "Vivian Hermann",
"username": "orville",
"id": 11,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/orville"
},
"subscribed": false,
"due_date": null,
"web_url": "http://example.com/example/example/issues/12",
"confidential": false,
"discussion_locked": false
}
```
## Create a todo
Manually creates a todo for the current user on an issue. If
......@@ -872,6 +919,7 @@ Example response:
"web_url": "http://example.com/example/example/issues/110",
"confidential": false,
"weight": null
"discussion_locked": false
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
......
......@@ -203,6 +203,7 @@ Parameters:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -280,6 +281,7 @@ Parameters:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -393,6 +395,7 @@ Parameters:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -499,6 +502,7 @@ order for it to take effect:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -529,8 +533,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `squash` | boolean| no | Squash commits into a single commit when merging |
Must include at least one non-required attribute from above.
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
Must include at least one non-required attribute from above.
......@@ -587,6 +590,7 @@ Must include at least one non-required attribute from above.
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -693,6 +697,7 @@ Parameters:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -897,6 +902,7 @@ Parameters:
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
......@@ -1192,7 +1198,8 @@ Example response:
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/francisca"
"web_url": "https://gitlab.example.com/francisca",
"discussion_locked": false
},
"assignee": {
"name": "Dr. Gabrielle Strosin",
......
......@@ -281,7 +281,7 @@ Point your users to the [after setup steps](after_setup.md).
## Selective replication
With GitLab 9.5, GitLab Geo now supports the first iteration of selective
replication, which allows admins to choose which namespaces should be
replication, which allows admins to choose which groups should be
replicated by secondary nodes.
It is important to notice that selective replication:
......@@ -291,7 +291,7 @@ It is important to notice that selective replication:
relies on PostgreSQL replication, all project metadata gets replicated to
secondary nodes, but repositories that have not been selected will be empty.
1. Secondary nodes won't pull repositories that do not belong to the selected
namespaces to be replicated.
groups to be replicated.
## Adding another secondary Geo node
......
doc/gitlab-geo/img/geo-architecture.png

59.7 KB | W: | H:

doc/gitlab-geo/img/geo-architecture.png

59.3 KB | W: | H:

doc/gitlab-geo/img/geo-architecture.png
doc/gitlab-geo/img/geo-architecture.png
doc/gitlab-geo/img/geo-architecture.png
doc/gitlab-geo/img/geo-architecture.png
  • 2-up
  • Swipe
  • Onion skin
*** NOTE: These instructions should be considered deprecated. In GitLab 10.0 we will be releasing new migration instructions using [pgloader](http://pgloader.io/).
---
last_updated: 2017-10-05
---
# Migrating GitLab from MySQL to Postgres
*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.*
# Migrating from MySQL to PostgreSQL
If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this.
> **Note:** This guide assumes you have a working Omnibus GitLab instance with
> MySQL and want to migrate to bundled PostgreSQL database.
## Export from MySQL and import into Postgres
## Prerequisites
Use this if you are keeping GitLab on the same server.
First, we'll need to enable the bundled PostgreSQL database with up-to-date
schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data
from the old MySQL database to the new PostgreSQL one.
```
sudo service gitlab stop
Here's what you'll need to have installed:
# Update /home/git/gitlab/config/database.yml
- pgloader 3.4.1+
- Omnibus GitLab
- MySQL
git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
cd mysql-postgresql-converter
mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql
ed -s gitlabhq_production.psql < move_drop_indexes.ed
## Enable bundled PostgreSQL database
# Import the database dump as the application database user
sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production
1. Stop GitLab:
# Install gems for PostgreSQL (note: the line below states '--without ... mysql')
sudo -u git -H bundle install --without development test mysql --deployment
``` bash
sudo gitlab-ctl stop
```
sudo service gitlab start
```
1. Edit `/etc/gitlab/gitlab.rb` to enable bundled PostgreSQL:
## Converting a GitLab backup file from MySQL to Postgres
**Note:** Please make sure to have Python 2.7.x (or higher) installed.
```
postgresql['enable'] = true
```
GitLab backup files (`<timestamp>_gitlab_backup.tar`) contain a SQL dump. Using the lanyrd database converter we can replace a MySQL database dump inside the tar file with a Postgres database dump. This can be useful if you are moving to another server.
1. Edit `/etc/gitlab/gitlab.rb` to use the bundled PostgreSQL. Please check
all the settings beginning with `db_`, such as `gitlab_rails['db_adapter']`
and alike. You could just comment all of them out so that we'll just use
the defaults.
```
# Stop GitLab
sudo service gitlab stop
1. [Reconfigure GitLab] for the changes to take effect:
``` bash
sudo gitlab-ctl reconfigure
```
1. Start Unicorn and PostgreSQL so that we could prepare the schema:
``` bash
sudo gitlab-ctl start unicorn
sudo gitlab-ctl start posgresql
```
1. Run the following commands to prepare the schema:
``` bash
sudo gitlab-rake db:create db:migrate
```
1. Stop Unicorn in case it's interfering the next step:
# Create the backup
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
``` bash
sudo gitlab-ctl stop unicorn
```
# Note the filename of the backup that was created. We will call it
# TIMESTAMP_gitlab_backup.tar below.
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
# Move the backup file we will convert to its own directory
sudo -u git -H mkdir -p tmp/backups/postgresql
sudo -u git -H mv tmp/backups/TIMESTAMP_gitlab_backup.tar tmp/backups/postgresql/
## Migrate data from MySQL to PostgreSQL
# Create a separate database dump with PostgreSQL compatibility
cd tmp/backups/postgresql
sudo -u git -H mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p
Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
# Clone the database converter
sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
1. Save the following snippet in a `commands.load` file, and edit with your
database `username`, `password` and `host`:
# Convert gitlabhq_production.mysql
sudo -u git -H mkdir db
sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql
sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed
```
LOAD DATABASE
FROM mysql://username:password@host/gitlabhq_production
INTO postgresql://gitlab-psql@unix://var/opt/gitlab/postgresql:/gitlabhq_production
# Compress database backup
# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with:
# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql
# The compressed databasedump is not supported at 7.12.0 and older.
sudo -u git -H gzip db/database.sql
WITH include no drop, truncate, disable triggers, create no tables,
create no indexes, preserve index names, no foreign keys,
data only
# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar.
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
# Warning: if you forget to replace TIMESTAMP below, tar will create a new file
# 'TIMESTAMP_gitlab_backup.tar' without giving an error.
;
```
sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz
1. Start the migration:
# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab
# installation.
# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups.
``` bash
sudo -u gitlab-psql pgloader commands.load
```
1. Once the migration finishes, start GitLab:
``` bash
sudo gitlab-ctl start
```
Now, you can verify that everything worked by visiting GitLab.
## Troubleshooting
### Experiencing 500 errors after the migration
If you experience 500 errors after the migration, try to clear the cache:
``` bash
sudo gitlab-rake cache:clear
```
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
......@@ -153,12 +153,52 @@ comments in greater detail.
![Discussion comment](img/discussion_comment.png)
## Locking discussions
> [Introduced][ce-14531] in GitLab 10.1.
There might be some cases where a discussion is better off if it's locked down.
For example:
- Discussions that are several years old and the issue/merge request is closed,
but people continue to try to resurrect the discussion.
- Discussions where someone or a group of people are trolling, are abusive, or
in-general are causing the discussion to be unproductive.
In locked discussions, only team members can write new comments and edit the old
ones.
To lock or unlock a discussion, you need to have at least Master [permissions]:
1. Find the "Lock" section in the sidebar and click **Edit**
1. In the dialog that will appear, you can choose to turn on or turn off the
discussion lock
1. Optionally, leave a comment to explain your reasoning behind that action
| Turn off discussion lock | Turn on discussion lock |
| :-----------: | :----------: |
| ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) |
Every change is indicated by a system note in the issue's or merge request's
comments.
![Discussion lock system notes](img/discussion_lock_system_notes.png)
Once an issue or merge request is locked, project members can see the indicator
in the comment area, whereas non project members can only see the information
that the discussion is locked.
| Team member | Not a member |
| :-----------: | :----------: |
| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
[discussion-view]: img/discussion_view.png
......
......@@ -26,6 +26,7 @@ The following table depicts the various user permission levels in a project.
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
| Lock comments | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
......
......@@ -14,9 +14,7 @@ class Admin::GeoNodesController < Admin::ApplicationController
end
def create
@node = GeoNode.new(geo_node_params)
if @node.save
if Geo::NodeCreateService.new(geo_node_params).execute
redirect_to admin_geo_nodes_path, notice: 'Node was successfully created.'
else
@nodes = GeoNode.all
......@@ -26,9 +24,9 @@ class Admin::GeoNodesController < Admin::ApplicationController
def update
if Geo::NodeUpdateService.new(@node, geo_node_params).execute
redirect_to admin_geo_nodes_path, notice: 'Geo Node was successfully updated.'
redirect_to admin_geo_nodes_path, notice: 'Node was successfully updated.'
else
render 'edit'
render :edit
end
end
......@@ -79,7 +77,7 @@ class Admin::GeoNodesController < Admin::ApplicationController
private
def geo_node_params
params.require(:geo_node).permit(:url, :primary, namespace_ids: [], geo_node_key_attributes: [:key])
params.require(:geo_node).permit(:url, :primary, :namespace_ids, geo_node_key_attributes: [:key])
end
def check_license
......
module EE
module GeoHelper
def node_namespaces_options(namespaces)
namespaces.map { |g| { id: g.id, text: g.full_name } }
end
def node_selected_namespaces_to_replicate(node)
node.namespaces.map(&:human_name).sort.join(', ')
end
......
This diff is collapsed.
This diff is collapsed.
......@@ -398,6 +398,7 @@ module API
expose :due_date
expose :confidential
expose :weight, if: ->(issue, _) { issue.supports_weight? }
expose :discussion_locked
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
......@@ -504,6 +505,7 @@ module API
expose :merge_commit_sha
expose :user_notes_count
expose :approvals_before_merge
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
......
......@@ -48,6 +48,7 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
end
params :issue_params_ee do
......@@ -200,7 +201,7 @@ module API
use :issue_params
at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event,
:weight
:weight, :discussion_locked
end
put ':id/issues/:issue_iid' do
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
......
......@@ -226,12 +226,14 @@ module API
:remove_source_branch,
:state_event,
:target_branch,
:title
:title,
:discussion_locked
]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
# EE
at_least_one_of_ee = [
......
......@@ -78,6 +78,8 @@ module API
}
if can?(current_user, noteable_read_ability_name(noteable), noteable)
authorize! :create_note, noteable
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
......
module Gitlab
module Geo
module LogHelpers
def log_info(message, details = {})
data = base_log_data(message)
data.merge!(details) if details
Gitlab::Geo::Logger.info(data)
end
def log_error(message, error)
data = base_log_data(message)
data[:error] = error.to_s
Gitlab::Geo::Logger.error(data)
end
protected
def base_log_data(message)
{
class: self.class.name,
message: message
}
end
end
end
end
module Gitlab
module Geo
module ProjectLogHelpers
def log_info(message, details = {})
data = base_log_data(message)
data.merge!(details) if details
Gitlab::Geo::Logger.info(data)
end
def log_error(message, error)
data = base_log_data(message)
data[:error] = error.to_s
Gitlab::Geo::Logger.error(data)
end
private
include LogHelpers
def base_log_data(message)
{
......
module Gitlab
module Geo
class Transfer
include LogHelpers
attr_reader :file_type, :file_id, :filename, :request_data
TEMP_PREFIX = 'tmp_'.freeze
def initialize(file_type, file_id, filename, request_data)
@file_type = file_type
@file_id = file_id
......@@ -50,34 +54,61 @@ module Gitlab
true
end
def log_transfer_error(message)
Rails.logger.error("#{self.class.name}: #{message}")
end
# Use HTTParty for now but switch to curb if performance becomes
# an issue
def download_file(url, req_headers)
file_size = -1
temp_file = open_temp_file(filename)
return unless temp_file
begin
File.open(filename, "wb") do |file|
response = HTTParty.get(url, headers: req_headers, stream_body: true) do |fragment|
file.write(fragment)
end
if response.success?
file_size = File.stat(filename).size
Rails.logger.info("GitLab Geo: Successfully downloaded #{filename} (#{file_size} bytes)")
else
log_transfer_error("Unsuccessful download: #{response.code} #{response.msg}")
end
response = HTTParty.get(url, headers: req_headers, stream_body: true) do |fragment|
temp_file.write(fragment)
end
temp_file.flush
unless response.success?
log_error("Unsuccessful download", response_code: response.code, response_msg: response.msg)
return file_size
end
if File.directory?(filename)
log_error("Destination file is a directory", filename: filename)
return file_size
end
FileUtils.mv(temp_file.path, filename)
file_size = File.stat(filename).size
log_info("Successful downloaded", filename: filename, file_size_bytes: file_size)
rescue StandardError, HTTParty::Error => e
log_transfer_error("Error downloading file: #{e}")
log_error("Error downloading file", error: e)
ensure
temp_file.close
temp_file.unlink
end
file_size
end
def default_permissions
0666 - File.umask
end
def open_temp_file(target_filename)
begin
# Make sure the file is in the same directory to prevent moves across filesystems
pathname = Pathname.new(target_filename)
temp = Tempfile.new(TEMP_PREFIX + pathname.basename.to_s, pathname.dirname.to_s)
temp.chmod(default_permissions)
temp.binmode
temp
rescue StandardError => e
log_error("Error creating temporary file", error: e)
end
end
end
end
end
......@@ -91,6 +91,11 @@ module Gitlab
end
end
def update_branch(branch_name, newrev, oldrev)
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
end
private
# Returns [newrev, should_run_after_create, should_run_after_create_branch]
......
require 'json'
module RspecFlaky
class Config
def self.generate_report?
ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
end
def self.suite_flaky_examples_report_path
ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/suite-report.json")
end
def self.flaky_examples_report_path
ENV['FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/report.json")
end
def self.new_flaky_examples_report_path
ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || Rails.root.join("rspec_flaky/new-report.json")
end
end
end
......@@ -9,24 +9,21 @@ module RspecFlaky
line: example.line,
description: example.description,
last_attempts_count: example.attempts,
flaky_reports: 1)
flaky_reports: 0)
else
super
end
end
def first_flaky_at
self[:first_flaky_at] || Time.now
end
def last_flaky_at
Time.now
end
def update_flakiness!(last_attempts_count: nil)
self.first_flaky_at ||= Time.now
self.last_flaky_at = Time.now
self.flaky_reports += 1
self.last_attempts_count = last_attempts_count if last_attempts_count
def last_flaky_job
return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
"#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
end
end
def to_h
......
require 'json'
module RspecFlaky
class FlakyExamplesCollection < SimpleDelegator
def self.from_json(json)
new(JSON.parse(json))
end
def initialize(collection = {})
unless collection.is_a?(Hash)
raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!"
end
collection_of_flaky_examples =
collection.map do |uid, example|
[
uid,
example.is_a?(RspecFlaky::FlakyExample) ? example : RspecFlaky::FlakyExample.new(example)
]
end
super(Hash[collection_of_flaky_examples])
end
def to_report
Hash[map { |uid, example| [uid, example.to_h] }].deep_symbolize_keys
end
def -(other)
unless other.respond_to?(:key)
raise ArgumentError, "`other` must respond to `#key?`, #{other.class} does not!"
end
self.class.new(reject { |uid, _| other.key?(uid) })
end
end
end
......@@ -2,11 +2,15 @@ require 'json'
module RspecFlaky
class Listener
attr_reader :all_flaky_examples, :new_flaky_examples
def initialize
@new_flaky_examples = {}
@all_flaky_examples = init_all_flaky_examples
# - suite_flaky_examples: contains all the currently tracked flacky example
# for the whole RSpec suite
# - flaky_examples: contains the examples detected as flaky during the
# current RSpec run
attr_reader :suite_flaky_examples, :flaky_examples
def initialize(suite_flaky_examples_json = nil)
@flaky_examples = FlakyExamplesCollection.new
@suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json)
end
def example_passed(notification)
......@@ -14,29 +18,21 @@ module RspecFlaky
return unless current_example.attempts > 1
flaky_example_hash = all_flaky_examples[current_example.uid]
all_flaky_examples[current_example.uid] =
if flaky_example_hash
FlakyExample.new(flaky_example_hash).tap do |ex|
ex.last_attempts_count = current_example.attempts
ex.flaky_reports += 1
end
else
FlakyExample.new(current_example).tap do |ex|
new_flaky_examples[current_example.uid] = ex
end
end
flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
flaky_examples[current_example.uid] = flaky_example
end
def dump_summary(_)
write_report_file(all_flaky_examples, all_flaky_examples_report_path)
write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path)
new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_report)
write_report_file(new_flaky_examples, new_flaky_examples_report_path)
write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
end
end
......@@ -46,30 +42,23 @@ module RspecFlaky
private
def init_all_flaky_examples
return {} unless File.exist?(all_flaky_examples_report_path)
def init_suite_flaky_examples(suite_flaky_examples_json = nil)
unless suite_flaky_examples_json
return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
suite_flaky_examples_json = File.read(RspecFlaky::Config.suite_flaky_examples_report_path)
end
Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
FlakyExamplesCollection.from_json(suite_flaky_examples_json)
end
def write_report_file(examples, file_path)
return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
def write_report_file(examples_collection, file_path)
return unless RspecFlaky::Config.generate_report?
report_path_dir = File.dirname(file_path)
FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
File.write(file_path, JSON.pretty_generate(to_report(examples)))
end
def all_flaky_examples_report_path
@all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] ||
Rails.root.join("rspec_flaky/all-report.json")
end
def new_flaky_examples_report_path
@new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] ||
Rails.root.join("rspec_flaky/new-report.json")
File.write(file_path, JSON.pretty_generate(examples_collection.to_report))
end
end
end
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-03 16:06-0400\n"
"PO-Revision-Date: 2017-10-03 16:06-0400\n"
"POT-Creation-Date: 2017-10-04 17:48+0100\n"
"PO-Revision-Date: 2017-10-04 17:48+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -382,6 +382,9 @@ msgstr ""
msgid "Cherry-pick this merge request"
msgstr ""
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
msgstr ""
msgid "CiStatusLabel|canceled"
msgstr ""
......@@ -712,6 +715,12 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
msgid "Geo|Groups to replicate"
msgstr ""
msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
......@@ -1195,6 +1204,21 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
......
......@@ -105,8 +105,10 @@ describe Admin::GeoNodesController, :postgresql do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
end
it 'creates the node' do
expect { go }.to change { GeoNode.count }.by(1)
it 'delegates the create of the Geo node to Geo::NodeCreateService' do
expect_any_instance_of(Geo::NodeCreateService).to receive(:execute).once.and_call_original
go
end
end
end
......
......@@ -266,6 +266,56 @@ describe Projects::NotesController do
end
end
end
context 'when the merge request discussion is locked' do
before do
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
merge_request.update_attribute(:discussion_locked, true)
end
context 'when a noteable is not found' do
it 'returns 404 status' do
request_params[:note][:noteable_id] = 9999
post :create, request_params.merge(format: :json)
expect(response).to have_http_status(404)
end
end
context 'when a user is a team member' do
it 'returns 302 status for html' do
post :create, request_params
expect(response).to have_http_status(302)
end
it 'returns 200 status for json' do
post :create, request_params.merge(format: :json)
expect(response).to have_http_status(200)
end
it 'creates a new note' do
expect { post :create, request_params }.to change { Note.count }.by(1)
end
end
context 'when a user is not a team member' do
before do
project.project_member(user).destroy
end
it 'returns 404 status' do
post :create, request_params
expect(response).to have_http_status(404)
end
it 'does not create a new note' do
expect { post :create, request_params }.not_to change { Note.count }
end
end
end
end
describe 'DELETE destroy' do
......
require 'spec_helper'
describe 'Discussion Lock', :js do
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project, author: user) }
let(:project) { create(:project, :public) }
before do
sign_in(user)
end
context 'when a user is a team member' do
before do
project.add_developer(user)
end
context 'when the discussion is unlocked' do
it 'the user can lock the issue' do
visit project_issue_path(project, issue)
expect(find('.issuable-sidebar')).to have_content('Unlocked')
page.within('.issuable-sidebar') do
find('.lock-edit').click
click_button('Lock')
end
expect(find('#notes')).to have_content('locked this issue')
end
end
context 'when the discussion is locked' do
before do
issue.update_attribute(:discussion_locked, true)
visit project_issue_path(project, issue)
end
it 'the user can unlock the issue' do
expect(find('.issuable-sidebar')).to have_content('Locked')
page.within('.issuable-sidebar') do
find('.lock-edit').click
click_button('Unlock')
end
expect(find('#notes')).to have_content('unlocked this issue')
expect(find('.issuable-sidebar')).to have_content('Unlocked')
end
it 'the user can create a comment' do
page.within('#notes .js-main-target-form') do
fill_in 'note[note]', with: 'Some new comment'
click_button 'Comment'
end
wait_for_requests
expect(find('div#notes')).to have_content('Some new comment')
end
end
end
context 'when a user is not a team member' do
context 'when the discussion is unlocked' do
before do
visit project_issue_path(project, issue)
end
it 'the user can not lock the issue' do
expect(find('.issuable-sidebar')).to have_content('Unlocked')
expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
end
it 'the user can create a comment' do
page.within('#notes .js-main-target-form') do
fill_in 'note[note]', with: 'Some new comment'
click_button 'Comment'
end
wait_for_requests
expect(find('div#notes')).to have_content('Some new comment')
end
end
context 'when the discussion is locked' do
before do
issue.update_attribute(:discussion_locked, true)
visit project_issue_path(project, issue)
end
it 'the user can not unlock the issue' do
expect(find('.issuable-sidebar')).to have_content('Locked')
expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
end
it 'the user can not create a comment' do
page.within('#notes') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.disabled-comment'))
.to have_content('This issue is locked. Only project members can comment.')
end
end
end
end
end
......@@ -645,14 +645,14 @@ describe 'Issues', :js do
visit project_issue_path(project, issue)
expect(page).to have_css('.confidential-issue-warning')
expect(page).to have_css('.is-confidential')
expect(page).not_to have_css('.is-not-confidential')
expect(page).to have_css('.issuable-note-warning')
expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
expect(page).to have_css('.confidential-warning-message')
expect(page).to have_css('.sidebar-item-warning-message')
within('.confidential-warning-message') do
within('.sidebar-item-warning-message') do
find('.btn-close').click
end
......@@ -660,7 +660,7 @@ describe 'Issues', :js do
visit project_issue_path(project, issue)
expect(page).not_to have_css('.is-confidential')
expect(page).not_to have_css('.is-active')
end
end
end
require 'spec_helper'
describe 'Discussion Lock', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
let(:project) { create(:project, :public, :repository) }
before do
sign_in(user)
end
context 'when the discussion is locked' do
before do
merge_request.update_attribute(:discussion_locked, true)
end
context 'when a user is a team member' do
before do
project.add_developer(user)
visit project_merge_request_path(project, merge_request)
end
it 'the user can create a comment' do
page.within('.issuable-discussion #notes .js-main-target-form') do
fill_in 'note[note]', with: 'Some new comment'
click_button 'Comment'
end
wait_for_requests
expect(find('.issuable-discussion #notes')).to have_content('Some new comment')
end
end
context 'when a user is not a team member' do
before do
visit project_merge_request_path(project, merge_request)
end
it 'the user can not create a comment' do
page.within('.issuable-discussion #notes') do
expect(page).not_to have_selector('js-main-target-form')
expect(page.find('.disabled-comment'))
.to have_content('This merge request is locked. Only project members can comment.')
end
end
end
end
end
......@@ -62,4 +62,23 @@ describe 'User accepts a merge request', :js do
wait_for_requests
end
end
context 'when modifying the merge commit message' do
before do
merge_request.mark_as_mergeable
visit(merge_request_path(merge_request))
end
it 'accepts a merge request' do
click_button('Modify commit message')
fill_in('Commit message', with: 'wow such merge')
click_button('Merge')
page.within('.status-box') do
expect(page).to have_content('Merged')
end
end
end
end
require 'spec_helper'
describe 'User approves a merge request', :js do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :repository, approvals_before_merge: 1) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
before do
project.add_developer(user)
sign_in(user)
end
context 'when user can approve' do
before do
visit(merge_request_path(merge_request))
end
it 'approves a merge request' do
page.within('.mr-state-widget') do
expect(page).to have_button('Merge', disabled: true)
click_button('Approve')
expect(page).to have_button('Merge', disabled: false)
end
end
end
context 'when user cannot approve' do
before do
merge_request.approvers.create(user_id: user2.id)
visit(merge_request_path(merge_request))
end
it 'does not approves a merge request' do
page.within('.mr-state-widget') do
expect(page).to have_button('Merge', disabled: true)
expect(page).not_to have_button('Approve')
expect(page).to have_content('Requires 1 more approval')
end
end
end
end
require 'spec_helper'
describe 'User closes a merge requests', :js do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(merge_request_path(merge_request))
end
it 'closes a merge request' do
click_link('Close merge request', match: :first)
expect(page).to have_content(merge_request.title)
expect(page).to have_content('Closed by')
end
end
require 'spec_helper'
describe 'User comments on a commit', :js do
include MergeRequestDiffHelpers
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(project_commit_path(project, sample_commit.id))
end
include_examples 'comment on merge request file'
end
require 'spec_helper'
describe 'User comments on a diff', :js do
include MergeRequestDiffHelpers
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
end
context 'when viewing comments' do
context 'when toggling inline comments' do
context 'in a single file' do
it 'hides a comment' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is wrong')
click_button('Comment')
end
page.within('.files > div:nth-child(3)') do
expect(page).to have_content('Line is wrong')
find('.js-toggle-diff-comments').trigger('click')
expect(page).not_to have_content('Line is wrong')
end
end
end
context 'in multiple files' do
it 'toggles comments' do
click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is correct')
click_button('Comment')
end
wait_for_requests
page.within('.files > div:nth-child(2) .note-body > .note-text') do
expect(page).to have_content('Line is correct')
end
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Line is wrong')
click_button('Comment')
end
wait_for_requests
# Hide the comment.
page.within('.files > div:nth-child(3)') do
find('.js-toggle-diff-comments').trigger('click')
expect(page).not_to have_content('Line is wrong')
end
# At this moment a user should see only one comment.
# The other one should be hidden.
page.within('.files > div:nth-child(2) .note-body > .note-text') do
expect(page).to have_content('Line is correct')
end
# Show the comment.
page.within('.files > div:nth-child(3)') do
find('.js-toggle-diff-comments').trigger('click')
end
# Now both the comments should be shown.
page.within('.files > div:nth-child(3) .note-body > .note-text') do
expect(page).to have_content('Line is wrong')
end
page.within('.files > div:nth-child(2) .note-body > .note-text') do
expect(page).to have_content('Line is correct')
end
# Check the same comments in the side-by-side view.
click_link('Side-by-side')
wait_for_requests
page.within('.files > div:nth-child(3) .parallel .note-body > .note-text') do
expect(page).to have_content('Line is wrong')
end
page.within('.files > div:nth-child(2) .parallel .note-body > .note-text') do
expect(page).to have_content('Line is correct')
end
end
end
end
end
context 'when adding comments' do
include_examples 'comment on merge request file'
end
context 'when editing comments' do
it 'edits a comment' do
click_diff_line(find("[id='#{sample_commit.line_code}']"))
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
click_button('Comment')
end
page.within('.diff-file:nth-of-type(5) .note') do
find('.js-note-edit').click
page.within('.current-note-edit-form') do
fill_in('note_note', with: 'Typo, please fix')
click_button('Save comment')
end
expect(page).not_to have_button('Save comment', disabled: true)
end
page.within('.diff-file:nth-of-type(5) .note') do
expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
end
end
end
context 'when deleting comments' do
it 'deletes a comment' do
click_diff_line(find("[id='#{sample_commit.line_code}']"))
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
click_button('Comment')
end
page.within('.notes-tab .badge') do
expect(page).to have_content('1')
end
page.within('.diff-file:nth-of-type(5) .note') do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
find('.js-note-delete').click
end
page.within('.merge-request-tabs') do
find('.notes-tab').trigger('click')
end
wait_for_requests
expect(page).not_to have_css('.notes .discussion')
page.within('.notes-tab .badge') do
expect(page).to have_content('0')
end
end
end
end
require 'spec_helper'
describe 'User comments on a merge request', :js do
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(merge_request_path(merge_request))
end
it 'adds a comment' do
page.within('.js-main-target-form') do
fill_in(:note_note, with: '# Comment with a header')
click_button('Comment')
end
wait_for_requests
page.within('.note') do
expect(page).to have_content('Comment with a header')
expect(page).not_to have_css('#comment-with-a-header')
end
end
it 'loads new comment' do
# Add new comment in background in order to check
# if it's going to be loaded automatically for current user.
create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong')
# Trigger a refresh of notes.
execute_script("$(document).trigger('visibilitychange');")
wait_for_requests
page.within('.notes .discussion') do
expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
expect(page).to have_content(sample_commit.line_code_path)
expect(page).to have_content('Line is wrong')
end
page.within('.notes-tab .badge') do
expect(page).to have_content('1')
end
end
end
require 'spec_helper'
describe 'User creates a merge request', :js do
let(:project) do
create(:project,
:repository,
approvals_before_merge: 1,
merge_requests_template: 'This merge request should contain the following.')
end
let(:user) { create(:user) }
let(:user2) { create(:user) }
before do
project.add_master(user)
sign_in(user)
project.approvers.create(user_id: user.id)
visit(project_new_merge_request_path(project))
end
it 'creates a merge request' do
allow_any_instance_of(Gitlab::AuthorityAnalyzer).to receive(:calculate).and_return([user2])
find('.js-source-branch').click
click_link('fix')
find('.js-target-branch').click
click_link('feature')
click_button('Compare branches')
expect(find_field('merge_request_description').value).to eq('This merge request should contain the following.')
# Approvers
page.within('ul .unsaved-approvers') do
expect(page).to have_content(user.name)
end
page.within('.suggested-approvers') do
expect(page).to have_content(user2.name)
end
click_link(user2.name)
page.within('ul.approver-list') do
expect(page).to have_content(user2.name)
end
# End of approvers
fill_in('merge_request_title', with: 'Wiki Feature')
click_button('Submit merge request')
page.within('.merge-request') do
expect(page).to have_content('Wiki Feature')
end
# wait_for_requests
page.within('.issuable-actions') do
click_link('Edit', match: :first)
end
page.within('ul.approver-list') do
expect(page).to have_content(user2.name)
end
end
end
require 'spec_helper'
describe 'User edits a merge request', :js do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(edit_project_merge_request_path(project, merge_request))
end
it 'changes the target branch' do
expect(page).to have_content('Target branch')
first('.target_branch').click
select('merge-test', from: 'merge_request_target_branch', visible: false)
click_button('Save changes')
expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
expect(page).to have_content("changed target branch from #{merge_request.target_branch} to merge-test")
end
end
require 'spec_helper'
describe 'User manages subscription', :js do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(merge_request_path(merge_request))
end
it 'toggles subscription' do
subscribe_button = find('.issuable-subscribe-button span')
expect(subscribe_button).to have_content('Subscribe')
click_on('Subscribe')
wait_for_requests
expect(subscribe_button).to have_content('Unsubscribe')
click_on('Unsubscribe')
wait_for_requests
expect(subscribe_button).to have_content('Subscribe')
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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