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';
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
Vue.use(Translate);
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
const confidentialEl = document.querySelector('#js-confidential-entry-point');
// 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);
}
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
if (!el) return;
if (confidentialEl) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(confidential);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
propsData: {
......@@ -31,16 +26,51 @@ function domContentLoaded() {
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(confidentialEl);
}).$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.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);
}
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
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,8 +18,10 @@ module Geo
def find_object_ids
downloaded_ids = find_downloaded_ids([:attachment, :avatar, :file])
current_node.uploads
.where.not(id: downloaded_ids)
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)
......@@ -29,14 +31,33 @@ module Geo
def find_lfs_object_ids
downloaded_ids = find_downloaded_ids([:lfs])
current_node.lfs_objects
.where.not(id: downloaded_ids)
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)
downloaded_ids = Geo::FileRegistry.where(file_type: file_types).pluck(:file_id)
(downloaded_ids + scheduled_file_ids(file_types)).uniq
......
---
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
......
@project_merge_requests
Feature: Project Merge Requests
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" have "Bug NS-04" open merge request
And project "Shop" have "Feature NS-03" closed merge request
And I visit project "Shop" merge requests page
Scenario: I should see open merge requests
Then I should see "Bug NS-04" in merge requests
And I should not see "Feature NS-03" in merge requests
Scenario: I should see CI status for merge requests
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
Given "Bug NS-05" has CI status
When I visit project "Shop" merge requests page
Then I should see merge request "Bug NS-05" with CI status
Scenario: I should not see target branch name when it is project's default branch
Then I should see "Bug NS-04" in merge requests
And I should not see "master" branch
Scenario: I should see target branch when it is different from default
Given project "Shop" have "Bug NS-06" open merge request
When I visit project "Shop" merge requests page
Then I should see "feature_conflict" branch
@javascript
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
When I visit merge request page "Bug NS-07"
Then I should not see the diverged commits count
@javascript
Scenario: I should see the numbers of diverged commits if the branch diverged from the target
Given project "Shop" have "Bug NS-08" open merge request with diverged branch
When I visit merge request page "Bug NS-08"
Then I should see the diverged commits count
@javascript
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
And I should not see "Bug NS-04" in merge requests
@javascript
Scenario: I should see all merge requests
Given I click link "All"
Then I should see "Feature NS-03" in merge requests
And I should see "Bug NS-04" in merge requests
@javascript
Scenario: I visit an open merge request page
Given I click link "Bug NS-04"
Then I should see merge request "Bug NS-04"
@javascript
Scenario: I visit a merged merge request page
Given project "Shop" have "Feature NS-05" merged merge request
And I click link "Merged"
And I click link "Feature NS-05"
Then I should see merge request "Feature NS-05"
@javascript
Scenario: I close merge request page
Given I click link "Bug NS-04"
And I click link "Close"
Then I should see closed merge request "Bug NS-04"
@javascript
Scenario: I reopen merge request page
Given I click link "Bug NS-04"
And I click link "Close"
Then I should see closed merge request "Bug NS-04"
When I click link "Reopen"
Then I should see reopened merge request "Bug NS-04"
@javascript
Scenario: I submit new unassigned merge request
Given I click link "New Merge Request"
And I submit new merge request "Wiki Feature"
Then I should see merge request "Wiki Feature"
@javascript
Scenario: I comment on a merge request
Given I visit merge request page "Bug NS-04"
And I leave a comment like "XML attached"
Then I should see comment "XML attached"
@javascript
Scenario: Visiting Merge Requests after being sorted the list
Given I visit project "Shop" merge requests page
And I sort the list by "Last updated"
And I visit my project's home page
And I visit project "Shop" merge requests page
Then The list should be sorted by "Last updated"
@javascript
Scenario: Visiting Merge Requests from a different Project after sorting
Given I visit project "Shop" merge requests page
And I sort the list by "Last updated"
And I visit dashboard merge requests page
Then The list should be sorted by "Last updated"
@javascript
Scenario: Sort merge requests by upvotes
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And project "Shop" have "Bug NS-06" open merge request
And merge request "Bug NS-04" have 2 upvotes and 1 downvote
And merge request "Bug NS-06" have 1 upvote and 2 downvotes
And I sort the list by "Popularity"
Then The list should be sorted by "Popularity"
@javascript
Scenario: I comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
And I switch to the merge request's comments tab
Then I should see a discussion has started on diff
And I should see a badge of "1" next to the discussion link
@javascript
Scenario: I see a new comment on merge request diff from another user in the discussion tab
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And user "John Doe" leaves a comment like "Line is wrong" on diff
Then I should see a discussion by user "John Doe" has started on diff
And I should see a badge of "1" next to the discussion link
@javascript
Scenario: I edit a comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
And I change the comment "Line is wrong" to "Typo, please fix" on diff
Then I should not see a diff comment saying "Line is wrong"
And I should see a diff comment saying "Typo, please fix"
@javascript
Scenario: I delete a comment on a merge request diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on diff
And I should see a badge of "1" next to the discussion link
And I delete the comment "Line is wrong" on diff
And I click on the Discussion tab
Then I should not see any discussion
And I should see a badge of "0" next to the discussion link
@javascript
Scenario: I comment on a line of a commit in merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the commit in the merge request
And I leave a comment like "Line is wrong" on diff in commit
And I switch to the merge request's comments tab
Then I should see a discussion has started on commit diff
@javascript
Scenario: I comment on a commit in merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the commit in the merge request
And I leave a comment on the diff page in commit
And I switch to the merge request's comments tab
Then I should see a discussion has started on commit
@javascript
Scenario: I accept merge request with custom commit message
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And merge request "Bug NS-05" is mergeable
And I visit merge request page "Bug NS-05"
And merge request is mergeable
Then I modify merge commit message
And I accept this merge request
Then I should see merged request
# Markdown
@javascript
Scenario: Headers inside the description should have ids generated for them.
When I visit merge request page "Bug NS-04"
Then Header "Description header" should have correct id and link
@javascript
Scenario: Headers inside comments should not have ids generated for them.
Given I visit merge request page "Bug NS-04"
And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID
# Toggling inline comments
@javascript
Scenario: I hide comments on a merge request diff with comments in a single file
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on line 39 of the third file
And I click link "Hide inline discussion" of the third file
Then I should not see a comment like "Line is wrong here" in the third file
@javascript
Scenario: I show comments on a merge request diff with comments in a single file
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is wrong" on line 39 of the third file
Then I should see a comment like "Line is wrong" in the third file
@javascript
Scenario: I hide comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is correct" on line 12 of the second file
And I leave a comment like "Line is wrong" on line 39 of the third file
And I click link "Hide inline discussion" of the third file
Then I should not see a comment like "Line is wrong here" in the third file
And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I show comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is correct" on line 12 of the second file
And I leave a comment like "Line is wrong" on line 39 of the third file
And I click link "Hide inline discussion" of the third file
And I click link "Show inline discussion" of the third file
Then I should see a comment like "Line is wrong" in the third file
And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I submit new unassigned merge request with template description
Given I click link "New Merge Request"
And I select "fix" as source
Then I should see description field pre-filled
@javascript
Scenario: I unfold diff
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I unfold diff
Then I should see additional file lines
@javascript
Scenario: I unfold diff in Side-by-Side view
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I click Side-by-side Diff tab
And I unfold diff
Then I should see additional file lines
@javascript
Scenario: I show comments on a merge request side-by-side diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
And I leave a comment like "Line is correct" on line 12 of the second file
And I leave a comment like "Line is wrong" on line 39 of the third file
And I click Side-by-side Diff tab
Then I should see comments on the side-by-side diff page
@javascript
Scenario: I view diffs on a merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
Then I should see the proper Inline and Side-by-side links
# Description preview
@javascript
Scenario: I can't preview without text
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I haven't written any description text
Then The Markdown preview tab should say there is nothing to do
@javascript
Scenario: I can preview with text
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I write a description like ":+1: Nice"
Then The Markdown preview tab should display rendered Markdown
@javascript
Scenario: I preview a merge request description
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown preview
And I should not see the Markdown text field
@javascript
Scenario: I can edit after preview
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
@javascript
Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04"
Then I should see that I am subscribed
When I click button "Unsubscribe"
Then I should see that I am unsubscribed
@javascript
Scenario: I can change the target branch
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
When I click the "Target branch" dropdown
And I select a new target branch
Then I should see new target branch changes
@javascript
Scenario: I can close merge request after commenting
Given I visit merge request page "Bug NS-04"
And I leave a comment like "XML attached"
Then I should see comment "XML attached"
And I click link "Close"
Then I should see closed merge request "Bug NS-04"
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
And I should see the merge button disabled
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
@javascript
Scenario: I can not approve merge request if I am not an approver
Given merge request 'Bug NS-04' must be approved by some user
And I click link "Bug NS-04"
And I should see the merge button disabled
When I should not see Approve button
And I should see message that MR require an approval
@javascript
Scenario: I see suggested approvers on new merge request form
Given I am a "Shop" developer
And project settings contain list of approvers
And I visit project "Shop" merge requests page
When I click link "New Merge Request"
And I select "feature_conflict" as source
Then I see suggested approver
@javascript
Scenario: I see auto-suggested approvers on new merge request form
Given I am a "Shop" developer
And project settings contain list of approvers
And there is one auto-suggested approver
And I visit project "Shop" merge requests page
When I click link "New Merge Request"
And I select "feature_conflict" as source
Then I see auto-suggested approver
And I can add it to approver list
class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedAuthentication
include SharedIssuable
include SharedProject
include SharedNote
include SharedPaths
include SharedMarkdown
include SharedDiffNote
include SharedUser
include WaitForRequests
after do
wait_for_requests if javascript_test?
end
step 'I click link "New Merge Request"' do
page.within '.nav-controls' do
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
end
step 'I click link "Bug NS-04"' do
click_link "Bug NS-04"
end
step 'I click link "Feature NS-05"' do
click_link "Feature NS-05"
end
step 'I click link "All"' do
find('.issues-state-filters [data-state="all"] span', text: 'All').click
# Waits for load
expect(find('.issues-state-filters > .active')).to have_content 'All'
end
step 'I click link "Merged"' do
find('#state-merged').trigger('click')
end
step 'I click link "Closed"' do
find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
end
step 'I should see merge request "Wiki Feature"' do
page.within '.merge-request' do
expect(page).to have_content "Wiki Feature"
end
wait_for_requests
end
step 'I should see closed merge request "Bug NS-04"' do
expect(page).to have_content "Bug NS-04"
expect(page).to have_content "Closed by"
wait_for_requests
end
step 'I should see merge request "Bug NS-04"' do
expect(page).to have_content "Bug NS-04"
wait_for_requests
end
step 'I should see merge request "Feature NS-05"' do
expect(page).to have_content "Feature NS-05"
wait_for_requests
end
step 'I should not see "master" branch' do
expect(find('.issuable-info')).not_to have_content "master"
end
step 'I should see "feature_conflict" branch' do
expect(page).to have_content "feature_conflict"
end
step 'I should see "Bug NS-04" in merge requests' do
expect(page).to have_content "Bug NS-04"
end
step 'I should see "Feature NS-03" in merge requests' do
expect(page).to have_content "Feature NS-03"
end
step 'I should not see "Feature NS-03" in merge requests' do
expect(page).not_to have_content "Feature NS-03"
end
step 'I should not see "Bug NS-04" in merge requests' do
expect(page).not_to have_content "Bug NS-04"
end
step 'I should see that I am subscribed' do
expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click button "Unsubscribe"' do
click_on "Unsubscribe"
wait_for_requests
end
step 'I click link "Close"' do
first(:css, '.close-mr-link').click
end
step 'I submit new merge request "Wiki Feature"' do
find('.js-source-branch').click
find('.dropdown-source-branch .dropdown-content a', text: 'fix').click
find('.js-target-branch').click
first('.dropdown-target-branch .dropdown-content a', text: 'feature').click
click_button "Compare branches"
fill_in "merge_request_title", with: "Wiki Feature"
click_button "Submit merge request"
end
step 'project "Shop" have "Bug NS-04" open merge request' do
create(:merge_request,
title: "Bug NS-04",
source_project: project,
target_project: project,
source_branch: 'fix',
target_branch: 'merge-test',
author: project.users.first,
description: "# Description header"
)
end
step 'project "Shop" have "Bug NS-06" open merge request' do
create(:merge_request,
title: "Bug NS-06",
source_project: project,
target_project: project,
source_branch: 'fix',
target_branch: 'feature_conflict',
author: project.users.first,
description: "# Description header"
)
end
step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
create(:merge_request_with_diffs,
title: "Bug NS-05",
source_project: project,
target_project: project,
author: project.users.first,
source_branch: 'merge-test')
end
step 'project "Shop" have "Feature NS-05" merged merge request' do
create(:merged_merge_request,
title: "Feature NS-05",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Bug NS-07" open merge request with rebased branch' do
create(:merge_request, :rebased,
title: "Bug NS-07",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Bug NS-08" open merge request with diverged branch' do
create(:merge_request, :diverged,
title: "Bug NS-08",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Shop" have "Feature NS-03" closed merge request' do
create(:closed_merge_request,
title: "Feature NS-03",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'project "Community" has "Bug CO-01" open merge request with diffs inside' do
project = Project.find_by(name: "Community")
create(:merge_request_with_diffs,
title: "Bug CO-01",
source_project: project,
target_project: project,
author: project.users.first)
end
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
merge_request = MergeRequest.find_by(title: 'Bug NS-04')
create_list(:award_emoji, 2, awardable: merge_request)
create(:award_emoji, :downvote, awardable: merge_request)
end
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
awardable = MergeRequest.find_by(title: 'Bug NS-06')
create(:award_emoji, awardable: awardable)
create_list(:award_emoji, 2, :downvote, awardable: awardable)
end
step 'The list should be sorted by "Least popular"' do
page.within '.mr-list' do
page.within 'li.merge-request:nth-child(1)' do
expect(page).to have_content 'Bug NS-06'
expect(page).to have_content '1 2'
end
page.within 'li.merge-request:nth-child(2)' do
expect(page).to have_content 'Bug NS-04'
expect(page).to have_content '2 1'
end
page.within 'li.merge-request:nth-child(3)' do
expect(page).to have_content 'Bug NS-05'
expect(page).not_to have_content '0 0'
end
end
end
step 'The list should be sorted by "Popularity"' do
page.within '.mr-list' do
page.within 'li.merge-request:nth-child(1)' do
expect(page).to have_content 'Bug NS-04'
expect(page).to have_content '2 1'
end
page.within 'li.merge-request:nth-child(2)' do
expect(page).to have_content 'Bug NS-06'
expect(page).to have_content '1 2'
end
page.within 'li.merge-request:nth-child(3)' do
expect(page).to have_content 'Bug NS-05'
expect(page).not_to have_content '0 0'
end
end
end
step 'I click on the Changes tab' do
page.within '.merge-request-tabs' do
click_link 'Changes'
end
# Waits for load
expect(page).to have_css('.tab-content #diffs.active')
end
step 'I should see the proper Inline and Side-by-side links' do
expect(page).to have_css('#parallel-diff-btn', count: 1)
expect(page).to have_css('#inline-diff-btn', count: 1)
end
step 'I switch to the merge request\'s comments tab' do
visit project_merge_request_path(project, merge_request)
end
step 'I click on the commit in the merge request' do
page.within '.merge-request-tabs' do
click_link 'Commits'
end
page.within '.commits' do
click_link Commit.truncate_sha(sample_commit.id)
end
end
step 'I leave a comment on the diff page' do
init_diff_note
leave_comment "One comment to rule them all"
end
step 'I leave a comment on the diff page in commit' do
click_diff_line(sample_commit.line_code)
leave_comment "One comment to rule them all"
end
step 'I leave a comment like "Line is wrong" on diff' do
init_diff_note
leave_comment "Line is wrong"
end
step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do
mr = MergeRequest.find_by(title: "Bug NS-05")
create(:diff_note_on_merge_request, project: project,
noteable: mr,
author: user_exists("John Doe"),
note: 'Line is wrong')
end
step 'I leave a comment like "Line is wrong" on diff in commit' do
click_diff_line(sample_commit.line_code)
leave_comment "Line is wrong"
end
step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do
page.within('.diff-file:nth-of-type(5) .note') do
find('.js-note-edit').click
page.within('.current-note-edit-form', visible: true) do
fill_in 'note_note', with: 'Typo, please fix'
click_button 'Save comment'
end
expect(page).not_to have_button 'Save comment', disabled: true, visible: true
end
end
step 'I should not see a diff comment saying "Line is wrong"' do
page.within('.diff-file:nth-of-type(5) .note') do
expect(page).not_to have_visible_content 'Line is wrong'
end
end
step 'I should see a diff comment saying "Typo, please fix"' do
page.within('.diff-file:nth-of-type(5) .note') do
expect(page).to have_visible_content 'Typo, please fix'
end
end
step 'I delete the comment "Line is wrong" on diff' do
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
end
step 'I click on the Discussion tab' do
page.within '.merge-request-tabs' do
find('.notes-tab').trigger('click')
end
# Waits for load
expect(page).to have_css('.tab-content #notes.active')
end
step 'I should not see any discussion' do
expect(page).not_to have_css('.notes .discussion')
end
step 'I should see a discussion has started on diff' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
end
step 'I should see a discussion by user "John Doe" has started on diff' do
# Trigger a refresh of notes
execute_script("$(document).trigger('visibilitychange');")
wait_for_requests
page.within(".notes .discussion") do
page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
end
step 'I should see a badge of "1" next to the discussion link' do
expect_discussion_badge_to_have_counter("1")
wait_for_requests
end
step 'I should see a badge of "0" next to the discussion link' do
expect_discussion_badge_to_have_counter("0")
wait_for_requests
end
step 'I should see a discussion has started on commit diff' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
wait_for_requests
end
end
step 'I should see a discussion has started on commit' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content "One comment to rule them all"
wait_for_requests
end
end
step 'merge request is mergeable' do
expect(page).to have_button 'Merge'
end
step 'I modify merge commit message' do
click_button "Modify commit message"
fill_in 'Commit message', with: 'wow such merge'
end
step 'merge request "Bug NS-05" is mergeable' do
merge_request.mark_as_mergeable
end
step 'I accept this merge request' do
page.within '.mr-state-widget' do
click_button "Merge"
end
end
step 'I should see merged request' do
page.within '.status-box' do
expect(page).to have_content "Merged"
wait_for_requests
end
end
step 'I click link "Reopen"' do
first(:css, '.reopen-mr-link').trigger('click')
end
step 'I should see reopened merge request "Bug NS-04"' do
page.within '.status-box' do
expect(page).to have_content "Open"
end
wait_for_requests
end
step 'I click link "Hide inline discussion" of the third file' do
page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I click link "Show inline discussion" of the third file' do
page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I should not see a comment like "Line is wrong" in the third file' do
page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong"
end
end
step 'I should see a comment like "Line is wrong" in the third file' do
page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
wait_for_requests
end
end
step 'I should not see a comment like "Line is wrong here" in the third file' do
page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong here"
end
end
step 'I should see a comment like "Line is wrong here" in the third file' do
page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong here"
end
end
step 'I leave a comment like "Line is correct" on line 12 of the second file' do
init_diff_note_first_file
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
end
step 'I leave a comment like "Line is wrong" on line 39 of the third file' do
init_diff_note_second_file
page.within(".js-discussion-note-form") do
fill_in "note_note", with: "Line is wrong on here"
click_button "Comment"
end
wait_for_requests
end
step 'I should still see a comment like "Line is correct" in the second file' do
page.within '.files>div:nth-child(2) .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
step 'I select "fix" as source' do
find('.js-source-branch').click
find('.dropdown-source-branch .dropdown-content a', text: 'fix').click
click_button "Compare branches"
end
step 'I select "feature_conflict" as source' do
find('.js-source-branch').click
find('.dropdown-source-branch .dropdown-content a', text: 'feature_conflict').click
click_button "Compare branches"
end
step 'I should see description field pre-filled' do
expect(find_field('merge_request_description').value).to eq 'This merge request should contain the following.'
end
step 'I unfold diff' do
expect(page).to have_css('.js-unfold')
first('.js-unfold').click
end
step 'I should see additional file lines' do
expect(first('.text-file')).to have_content('.bundle')
end
step 'I click Side-by-side Diff tab' do
find('a', text: 'Side-by-side').trigger('click')
# Waits for load
expect(page).to have_css('.parallel')
end
step 'I should see comments on the side-by-side diff page' do
page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
wait_for_requests
end
end
step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe"
page.within '.merge-requests-holder' do
find('.merge-request')
end
end
step 'I click the "Target branch" dropdown' do
expect(page).to have_content('Target branch')
first('.target_branch').click
end
step 'I select a new target branch' do
select "feature", from: "merge_request_target_branch"
click_button 'Save'
end
step 'I should see new target branch changes' do
expect(page).to have_content 'Request to merge fix into feature'
expect(page).to have_content 'changed target branch from merge-test to feature'
wait_for_requests
end
step 'project settings contain list of approvers' do
project.update(approvals_before_merge: 1)
project.approvers.create(user_id: current_user.id)
end
step 'there is one auto-suggested approver' do
@user = create :user
allow_any_instance_of(Gitlab::AuthorityAnalyzer).to receive(:calculate).and_return([@user])
end
step 'I see suggested approver' do
page.within 'ul .unsaved-approvers' do
expect(page).to have_content(current_user.name)
end
end
step 'I see auto-suggested approver' do
page.within '.suggested-approvers' do
expect(page).to have_content(@user.name)
end
end
step 'I can add it to approver list' do
click_link @user.name
page.within 'ul.approver-list' do
expect(page).to have_content(@user.name)
end
click_button "Submit merge request"
page.within '.issuable-actions' do
first(:link, 'Edit', visible: true).click
end
page.within 'ul.approver-list' do
expect(page).to have_content(@user.name)
end
end
step 'merge request \'Bug NS-04\' must be approved' do
merge_request = MergeRequest.find_by!(title: "Bug NS-04")
project = merge_request.target_project
project.approvals_before_merge = 1
project.save!
end
step 'merge request \'Bug NS-04\' must be approved by current user' do
merge_request = MergeRequest.find_by!(title: "Bug NS-04")
project = merge_request.target_project
project.approvals_before_merge = 1
merge_request.approvers.create(user_id: current_user.id)
project.save!
end
step 'merge request \'Bug NS-04\' must be approved by some user' do
merge_request = MergeRequest.find_by!(title: "Bug NS-04")
project = merge_request.target_project
project.approvals_before_merge = 1
merge_request.approvers.create(user_id: create(:user).id)
project.save!
end
step 'I click link "Approve"' do
page.within '.mr-state-widget' do
wait_for_requests
click_button 'Approve'
end
end
step 'I should see the merge button disabled' do
page.within '.mr-state-widget' do
expect(page).to have_button('Merge', disabled: true)
end
end
step 'I should not see merge button' do
page.within '.mr-state-widget' do
expect(page).not_to have_button('Accept merge mequest')
end
end
step 'I should not see Approve button' do
page.within '.mr-state-widget' do
expect(page).not_to have_button("Approve")
end
wait_for_requests
end
step 'I should see approved merge request "Bug NS-04"' do
page.within '.mr-state-widget' do
expect(page).to have_button('Merge', disabled: false)
end
wait_for_requests
end
step 'I should see message that merge request can be merged' do
page.within '.mr-state-widget' do
expect(page).to have_content("Ready to be merged automatically")
end
end
step 'I should see message that MR require an approval from me' do
page.within '.mr-state-widget' do
expect(page).to have_content("Requires 1 more approval (from #{current_user.name})")
end
end
step 'I should see message that MR require an approval' do
page.within '.mr-state-widget' do
expect(page).to have_content("Requires 1 more approval")
wait_for_requests
end
end
step 'I click on "Email Patches"' do
click_link "Email Patches"
end
step 'I click on "Plain Diff"' do
click_link "Plain Diff"
end
step 'I should see a patch diff' do
expect(page).to have_content('diff --git')
end
step 'I am a "Shop" developer' do
user = create(:user, name: "Mike")
project = Project.find_by(name: "Shop")
project.team << [user, :developer]
gitlab_sign_out
sign_in(user)
end
step '"Bug NS-05" has CI status' do
project = merge_request.source_project
project.enable_ci
pipeline =
create(:ci_pipeline,
project: project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch,
head_pipeline_of: merge_request)
create :ci_build, pipeline: pipeline
end
step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do
expect(page).to have_link "Pipeline: pending"
end
end
step 'I should see the diverged commits count' do
page.within ".mr-source-target" do
expect(page).to have_content /([0-9]+ commits behind)/
end
wait_for_requests
end
step 'I should not see the diverged commits count' do
page.within ".mr-source-target" do
expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
wait_for_requests
end
wait_for_requests
end
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
def init_diff_note
click_diff_line(sample_commit.line_code)
end
def leave_comment(message)
page.within(".js-discussion-note-form", visible: true) do
fill_in "note_note", with: message
click_button "Comment"
end
wait_for_requests
page.within(".notes_holder", visible: true) do
expect(page).to have_content message
end
end
def init_diff_note_first_file
click_diff_line(sample_compare.changes[0][:line_code])
end
def init_diff_note_second_file
click_diff_line(sample_compare.changes[1][:line_code])
end
def have_visible_content(text)
have_css("*", text: text, visible: true)
end
def expect_discussion_badge_to_have_counter(value)
page.within(".notes-tab .badge") do
page.should have_content value
end
end
end
......@@ -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)
temp_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}")
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 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_at
Time.now
if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
end
def last_flaky_job
return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
"#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
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]
flaky_example = suite_flaky_examples.fetch(current_example.uid) { FlakyExample.new(current_example) }
flaky_example.update_flakiness!(last_attempts_count: current_example.attempts)
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_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
require 'spec_helper'
describe 'User reopens a merge requests', :js do
let(:project) { create(:project, :public, :repository) }
let!(:merge_request) { create(:closed_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 'reopens a merge request' do
click_link('Reopen merge request', match: :first)
page.within('.status-box') do
expect(page).to have_content('Open')
end
end
end
require 'spec_helper'
describe 'User sorts merge requests' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:merge_request2) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(project_merge_requests_path(project))
end
it 'keeps the sort option' do
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link('Last updated')
end
visit(merge_requests_dashboard_path(assignee_id: user.id))
expect(find('.issues-filters')).to have_content('Last updated')
visit(project_merge_requests_path(project))
expect(find('.issues-filters')).to have_content('Last updated')
end
context 'when merge requests have awards' do
before do
create_list(:award_emoji, 2, awardable: merge_request)
create(:award_emoji, :downvote, awardable: merge_request)
create(:award_emoji, awardable: merge_request2)
create_list(:award_emoji, 2, :downvote, awardable: merge_request2)
end
it 'sorts by popularity' do
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link('Popularity')
end
page.within('.mr-list') do
page.within('li.merge-request:nth-child(1)') do
expect(page).to have_content(merge_request.title)
expect(page).to have_content('2 1')
end
page.within('li.merge-request:nth-child(2)') do
expect(page).to have_content(merge_request2.title)
expect(page).to have_content('1 2')
end
end
end
end
end
require 'spec_helper'
describe 'User views all merge requests' do
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
before do
visit(project_merge_requests_path(project, state: :all))
end
it 'shows all merge requests' do
expect(page).to have_content(merge_request.title).and have_content(closed_merge_request.title)
end
end
require 'spec_helper'
describe 'User views closed merge requests' do
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
before do
visit(project_merge_requests_path(project, state: :closed))
end
it 'shows closed merge requests' do
expect(page).to have_content(closed_merge_request.title).and have_no_content(merge_request.title)
end
end
require 'spec_helper'
describe 'User views diffs', :js do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
let(:project) { create(:project, :public, :repository) }
before do
visit(diffs_project_merge_request_path(project, merge_request))
wait_for_requests
end
shared_examples 'unfold diffs' do
it 'unfolds diffs' do
first('.js-unfold').click
expect(first('.text-file')).to have_content('.bundle')
end
end
it 'shows diffs' do
expect(page).to have_css('.tab-content #diffs.active')
expect(page).to have_css('#parallel-diff-btn', count: 1)
expect(page).to have_css('#inline-diff-btn', count: 1)
end
context 'when in the inline view' do
include_examples 'unfold diffs'
end
context 'when in the side-by-side view' do
before do
click_link('Side-by-side')
wait_for_requests
end
it 'shows diffs in parallel' do
expect(page).to have_css('.parallel')
end
include_examples 'unfold diffs'
end
end
require 'spec_helper'
describe 'User views merged merge requests' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:merged_merge_request) { create(:merged_merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
before do
visit(project_merge_requests_path(project, state: :merged))
end
it 'shows merged merge requests' do
expect(page).to have_content(merged_merge_request.title).and have_no_content(merge_request.title)
end
end
require 'spec_helper'
describe 'User views an open merge request' do
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project, description: '# Description header')
end
context 'when a merge request does not have repository' do
let(:project) { create(:project, :public) }
before do
visit(merge_request_path(merge_request))
end
it 'renders both the title and the description' do
node = find('.wiki h1 a#user-content-description-header')
expect(node[:href]).to end_with('#description-header')
# Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element
expect(find(:xpath, "#{node.path}/..").text).to eq(merge_request.description[2..-1])
expect(page).to have_content(merge_request.title).and have_content(merge_request.description)
end
end
context 'when a merge request has repository', :js do
let(:project) { create(:project, :public, :repository) }
context 'when rendering description preview' do
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
visit(edit_project_merge_request_path(project, merge_request))
end
it 'renders empty description preview' do
find('.gfm-form').fill_in(:merge_request_description, with: '')
page.within('.gfm-form') do
click_link('Preview')
expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end
end
it 'renders description preview' do
find('.gfm-form').fill_in(:merge_request_description, with: ':+1: Nice')
page.within('.gfm-form') do
click_link('Preview')
expect(find('.js-md-preview')).to have_css('gl-emoji')
end
expect(find('.gfm-form')).to have_css('.js-md-preview').and have_link('Write')
expect(find('#merge_request_description', visible: false)).not_to be_visible
end
end
context 'when the branch is rebased on the target' do
let(:merge_request) { create(:merge_request, :rebased, source_project: project, target_project: project) }
before do
visit(merge_request_path(merge_request))
end
it 'does not show diverged commits count' do
page.within('.mr-source-target') do
expect(page).not_to have_content(/([0-9]+ commit[s]? behind)/)
end
end
end
context 'when the branch is diverged on the target' do
let(:merge_request) { create(:merge_request, :diverged, source_project: project, target_project: project) }
before do
visit(merge_request_path(merge_request))
end
it 'shows diverged commits count' do
page.within('.mr-source-target') do
expect(page).to have_content(/([0-9]+ commits behind)/)
end
end
end
end
end
require 'spec_helper'
describe 'User views open merge requests' do
let(:project) { create(:project, :public, :repository) }
context "when the target branch is the project's default branch" do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
before do
visit(project_merge_requests_path(project))
end
it 'shows open merge requests' do
expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
end
it 'does not show target branch name' do
expect(page).to have_content(merge_request.title)
expect(find('.issuable-info')).not_to have_content(project.default_branch)
end
end
context "when the target branch is different from the project's default branch" do
let!(:merge_request) do
create(:merge_request,
source_project: project,
target_project: project,
source_branch: 'fix',
target_branch: 'feature_conflict')
end
before do
visit(project_merge_requests_path(project))
end
it 'shows target branch name' do
expect(page).to have_content(merge_request.target_branch)
end
end
context 'when a merge request has pipelines' do
let!(:build) { create :ci_build, pipeline: pipeline }
let(:merge_request) do
create(:merge_request_with_diffs,
source_project: project,
target_project: project,
source_branch: 'merge-test')
end
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch,
head_pipeline_of: merge_request)
end
before do
project.enable_ci
visit(project_merge_requests_path(project))
end
it 'shows pipeline status' do
page.within('.mr-list') do
expect(page).to have_link('Pipeline: pending')
end
end
end
end
......@@ -9,6 +9,7 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"discussion_locked": { "type": ["boolean", "null"] },
"closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
......
......@@ -72,6 +72,7 @@
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"approvals_before_merge": { "type": ["integer", "null"] },
"squash": { "type": "boolean" },
......
import Vue from 'vue';
import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('EditFormButtons', () => {
let vm1;
let vm2;
beforeEach(() => {
const Component = Vue.extend(editFormButtons);
const toggleForm = () => { };
const updateLockedAttribute = () => { };
vm1 = mountComponent(Component, {
isLocked: true,
toggleForm,
updateLockedAttribute,
});
vm2 = mountComponent(Component, {
isLocked: false,
toggleForm,
updateLockedAttribute,
});
});
it('renders unlock or lock text based on locked state', () => {
expect(
vm1.$el.innerHTML.includes('Unlock'),
).toBe(true);
expect(
vm2.$el.innerHTML.includes('Lock'),
).toBe(true);
});
});
import Vue from 'vue';
import editForm from '~/sidebar/components/lock/edit_form.vue';
describe('EditForm', () => {
let vm1;
let vm2;
beforeEach(() => {
const Component = Vue.extend(editForm);
const toggleForm = () => { };
const updateLockedAttribute = () => { };
vm1 = new Component({
propsData: {
isLocked: true,
toggleForm,
updateLockedAttribute,
issuableType: 'issue',
},
}).$mount();
vm2 = new Component({
propsData: {
isLocked: false,
toggleForm,
updateLockedAttribute,
issuableType: 'merge_request',
},
}).$mount();
});
it('renders on the appropriate warning text', () => {
expect(
vm1.$el.innerHTML.includes('Unlock this issue?'),
).toBe(true);
expect(
vm2.$el.innerHTML.includes('Lock this merge request?'),
).toBe(true);
});
});
import Vue from 'vue';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
beforeEach(() => {
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
service: {
update: Promise.resolve(true),
},
store: {
isLockDialogOpen: false,
},
};
vm1 = new Component({
propsData: {
isLocked: true,
isEditable: true,
mediator,
issuableType: 'issue',
},
}).$mount();
vm2 = new Component({
propsData: {
isLocked: false,
isEditable: false,
mediator,
issuableType: 'merge_request',
},
}).$mount();
});
it('shows if locked and/or editable', () => {
expect(
vm1.$el.innerHTML.includes('Edit'),
).toBe(true);
expect(
vm1.$el.innerHTML.includes('Locked'),
).toBe(true);
expect(
vm2.$el.innerHTML.includes('Unlocked'),
).toBe(true);
});
it('displays the edit form when editable', (done) => {
expect(vm1.isLockDialogOpen).toBe(false);
vm1.$el.querySelector('.lock-edit').click();
expect(vm1.isLockDialogOpen).toBe(true);
vm1.$nextTick(() => {
expect(
vm1.$el
.innerHTML
.includes('Unlock this issue?'),
).toBe(true);
done();
});
});
});
import Vue from 'vue';
import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue';
describe('Confidential Issue Warning Component', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(confidentialIssue);
vm = new Component().$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render confidential issue warning information', () => {
expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash');
expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
});
});
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
function formatWarning(string) {
// Replace newlines with a space then replace multiple spaces with one space
return string.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' ');
}
describe('Issue Warning Component', () => {
describe('isLocked', () => {
it('should render locked issue warning information', () => {
const vm = mountComponent(IssueWarning, {
isLocked: true,
});
expect(vm.$el.querySelector('i').className).toEqual('fa fa-lock');
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
});
});
describe('isConfidential', () => {
it('should render confidential issue warning information', () => {
const vm = mountComponent(IssueWarning, {
isConfidential: true,
});
expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash');
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
});
});
describe('isLocked and isConfidential', () => {
it('should render locked and confidential issue warning information', () => {
const vm = mountComponent(IssueWarning, {
isLocked: true,
isConfidential: true,
});
expect(vm.$el.querySelector('i')).toBeFalsy();
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
});
});
});
......@@ -7,13 +7,9 @@ describe Gitlab::Geo::Transfer do
set(:secondary_node) { create(:geo_node) }
set(:lfs_object) { create(:lfs_object, :with_file) }
let(:url) { primary_node.geo_transfers_url(:lfs, lfs_object.id.to_s) }
let(:content) { StringIO.new("1\n2\n3") }
let(:content) { SecureRandom.random_bytes(10) }
let(:size) { File.stat(lfs_object.file.path).size }
before do
allow(File).to receive(:open).with(lfs_object.file.path, "wb").and_yield(content)
end
subject do
described_class.new(:lfs,
lfs_object.id,
......@@ -21,12 +17,35 @@ describe Gitlab::Geo::Transfer do
{ sha256: lfs_object.oid })
end
it '#download_from_primary' do
context '#download_from_primary' do
before do
stub_current_geo_node(secondary_node)
end
it 'when the destination filename is a directory' do
transfer = described_class.new(:lfs, lfs_object.id, '/tmp', { sha256: lfs_object.id })
expect(transfer.download_from_primary).to eq(nil)
end
it 'when the HTTP response is successful' do
expect(FileUtils).to receive(:mv).with(anything, lfs_object.file.path).and_call_original
response = double(success?: true)
expect(HTTParty).to receive(:get).and_return(response)
expect(HTTParty).to receive(:get).and_yield(content.to_s).and_return(response)
expect(subject.download_from_primary).to eq(size)
stat = File.stat(lfs_object.file.path)
expect(stat.size).to eq(size)
expect(stat.mode & 0777).to eq(0666 - File.umask)
expect(File.binread(lfs_object.file.path)).to eq(content)
end
it 'when the HTTP response is unsuccessful' do
expect(FileUtils).not_to receive(:mv).with(anything, lfs_object.file.path).and_call_original
response = double(success?: false, code: 404, msg: 'No such file')
expect(HTTParty).to receive(:get).and_return(response)
expect(subject.download_from_primary).to eq(-1)
end
end
end
......@@ -26,6 +26,7 @@ Issue:
- service_desk_reply_to
- last_edited_at
- last_edited_by_id
- discussion_locked
Event:
- id
- target_type
......@@ -172,6 +173,7 @@ MergeRequest:
- last_edited_at
- last_edited_by_id
- head_pipeline_id
- discussion_locked
MergeRequestDiff:
- id
- state
......
require 'spec_helper'
describe RspecFlaky::Config, :aggregate_failures do
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
end
describe '.generate_report?' do
context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is not set" do
it 'returns false' do
expect(described_class).not_to be_generate_report
end
end
context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'false'" do
before do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
end
it 'returns false' do
expect(described_class).not_to be_generate_report
end
end
context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set to 'true'" do
before do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
end
it 'returns true' do
expect(described_class).to be_generate_report
end
end
end
describe '.suite_flaky_examples_report_path' do
context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do
expect(Rails.root).to receive(:join).with('rspec_flaky/suite-report.json')
.and_return('root/rspec_flaky/suite-report.json')
expect(described_class.suite_flaky_examples_report_path).to eq('root/rspec_flaky/suite-report.json')
end
end
context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do
before do
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json')
end
it 'returns the value of the env variable' do
expect(described_class.suite_flaky_examples_report_path).to eq('foo/suite-report.json')
end
end
end
describe '.flaky_examples_report_path' do
context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do
expect(Rails.root).to receive(:join).with('rspec_flaky/report.json')
.and_return('root/rspec_flaky/report.json')
expect(described_class.flaky_examples_report_path).to eq('root/rspec_flaky/report.json')
end
end
context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is set" do
before do
stub_env('FLAKY_RSPEC_REPORT_PATH', 'foo/report.json')
end
it 'returns the value of the env variable' do
expect(described_class.flaky_examples_report_path).to eq('foo/report.json')
end
end
end
describe '.new_flaky_examples_report_path' do
context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do
expect(Rails.root).to receive(:join).with('rspec_flaky/new-report.json')
.and_return('root/rspec_flaky/new-report.json')
expect(described_class.new_flaky_examples_report_path).to eq('root/rspec_flaky/new-report.json')
end
end
context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is set" do
before do
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', 'foo/new-report.json')
end
it 'returns the value of the env variable' do
expect(described_class.new_flaky_examples_report_path).to eq('foo/new-report.json')
end
end
end
end
require 'spec_helper'
describe RspecFlaky::FlakyExample do
describe RspecFlaky::FlakyExample, :aggregate_failures do
let(:flaky_example_attrs) do
{
example_id: 'spec/foo/bar_spec.rb:2',
......@@ -9,6 +9,7 @@ describe RspecFlaky::FlakyExample do
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: 2345,
last_flaky_job: 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/12',
last_attempts_count: 2,
flaky_reports: 1
}
......@@ -27,57 +28,78 @@ describe RspecFlaky::FlakyExample do
end
let(:example) { double(example_attrs) }
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
end
describe '#initialize' do
shared_examples 'a valid FlakyExample instance' do
it 'returns valid attributes' do
flaky_example = described_class.new(args)
let(:flaky_example) { described_class.new(args) }
it 'returns valid attributes' do
expect(flaky_example.uid).to eq(flaky_example_attrs[:uid])
expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id])
expect(flaky_example.file).to eq(flaky_example_attrs[:file])
expect(flaky_example.line).to eq(flaky_example_attrs[:line])
expect(flaky_example.description).to eq(flaky_example_attrs[:description])
expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
expect(flaky_example.last_flaky_at).to eq(expected_last_flaky_at)
expect(flaky_example.last_attempts_count).to eq(flaky_example_attrs[:last_attempts_count])
expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
end
end
context 'when given an Rspec::Example' do
it_behaves_like 'a valid FlakyExample instance' do
let(:args) { example }
it_behaves_like 'a valid FlakyExample instance'
let(:expected_first_flaky_at) { nil }
let(:expected_last_flaky_at) { nil }
let(:expected_flaky_reports) { 0 }
end
end
context 'when given a hash' do
it_behaves_like 'a valid FlakyExample instance' do
let(:args) { flaky_example_attrs }
it_behaves_like 'a valid FlakyExample instance'
let(:expected_flaky_reports) { flaky_example_attrs[:flaky_reports] }
let(:expected_first_flaky_at) { flaky_example_attrs[:first_flaky_at] }
let(:expected_last_flaky_at) { flaky_example_attrs[:last_flaky_at] }
end
end
describe '#to_h' do
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
end
shared_examples 'a valid FlakyExample hash' do
let(:additional_attrs) { {} }
describe '#update_flakiness!' do
shared_examples 'an up-to-date FlakyExample instance' do
let(:flaky_example) { described_class.new(args) }
it 'returns a valid hash' do
flaky_example = described_class.new(args)
final_hash = flaky_example_attrs
.merge(last_flaky_at: instance_of(Time), last_flaky_job: nil)
.merge(additional_attrs)
it 'updates the first_flaky_at' do
now = Time.now
expected_first_flaky_at = flaky_example.first_flaky_at ? flaky_example.first_flaky_at : now
Timecop.freeze(now) { flaky_example.update_flakiness! }
expect(flaky_example.to_h).to match(hash_including(final_hash))
expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
end
it 'updates the last_flaky_at' do
now = Time.now
Timecop.freeze(now) { flaky_example.update_flakiness! }
expect(flaky_example.last_flaky_at).to eq(now)
end
context 'when given an Rspec::Example' do
let(:args) { example }
it 'updates the flaky_reports' do
expected_flaky_reports = flaky_example.first_flaky_at ? flaky_example.flaky_reports + 1 : 1
context 'when run locally' do
it_behaves_like 'a valid FlakyExample hash' do
let(:additional_attrs) do
{ first_flaky_at: instance_of(Time) }
expect { flaky_example.update_flakiness! }.to change { flaky_example.flaky_reports }.by(1)
expect(flaky_example.flaky_reports).to eq(expected_flaky_reports)
end
context 'when passed a :last_attempts_count' do
it 'updates the last_attempts_count' do
flaky_example.update_flakiness!(last_attempts_count: 42)
expect(flaky_example.last_attempts_count).to eq(42)
end
end
......@@ -87,10 +109,45 @@ describe RspecFlaky::FlakyExample do
stub_env('CI_JOB_ID', 42)
end
it 'updates the last_flaky_job' do
flaky_example.update_flakiness!
expect(flaky_example.last_flaky_job).to eq('https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42')
end
end
end
context 'when given an Rspec::Example' do
it_behaves_like 'an up-to-date FlakyExample instance' do
let(:args) { example }
end
end
context 'when given a hash' do
it_behaves_like 'an up-to-date FlakyExample instance' do
let(:args) { flaky_example_attrs }
end
end
end
describe '#to_h' do
shared_examples 'a valid FlakyExample hash' do
let(:additional_attrs) { {} }
it 'returns a valid hash' do
flaky_example = described_class.new(args)
final_hash = flaky_example_attrs.merge(additional_attrs)
expect(flaky_example.to_h).to eq(final_hash)
end
end
context 'when given an Rspec::Example' do
let(:args) { example }
it_behaves_like 'a valid FlakyExample hash' do
let(:additional_attrs) do
{ first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" }
end
{ first_flaky_at: nil, last_flaky_at: nil, last_flaky_job: nil, flaky_reports: 0 }
end
end
end
......
require 'spec_helper'
describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do
let(:collection_hash) do
{
a: { example_id: 'spec/foo/bar_spec.rb:2' },
b: { example_id: 'spec/foo/baz_spec.rb:3' }
}
end
let(:collection_report) do
{
a: {
example_id: 'spec/foo/bar_spec.rb:2',
first_flaky_at: nil,
last_flaky_at: nil,
last_flaky_job: nil
},
b: {
example_id: 'spec/foo/baz_spec.rb:3',
first_flaky_at: nil,
last_flaky_at: nil,
last_flaky_job: nil
}
}
end
describe '.from_json' do
it 'accepts a JSON' do
collection = described_class.from_json(JSON.pretty_generate(collection_hash))
expect(collection.to_report).to eq(described_class.new(collection_hash).to_report)
end
end
describe '#initialize' do
it 'accepts no argument' do
expect { described_class.new }.not_to raise_error
end
it 'accepts a hash' do
expect { described_class.new(collection_hash) }.not_to raise_error
end
it 'does not accept anything else' do
expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`collection` must be a Hash, Array given!")
end
end
describe '#to_report' do
it 'calls #to_h on the values' do
collection = described_class.new(collection_hash)
expect(collection.to_report).to eq(collection_report)
end
end
describe '#-' do
it 'returns only examples that are not present in the given collection' do
collection1 = described_class.new(collection_hash)
collection2 = described_class.new(
a: { example_id: 'spec/foo/bar_spec.rb:2' },
c: { example_id: 'spec/bar/baz_spec.rb:4' })
expect((collection2 - collection1).to_report).to eq(
c: {
example_id: 'spec/bar/baz_spec.rb:4',
first_flaky_at: nil,
last_flaky_at: nil,
last_flaky_job: nil
})
end
it 'fails if the given collection does not respond to `#key?`' do
collection = described_class.new(collection_hash)
expect { collection - [1, 2, 3] }.to raise_error(ArgumentError, "`other` must respond to `#key?`, Array does not!")
end
end
end
require 'spec_helper'
describe RspecFlaky::Listener do
let(:flaky_example_report) do
describe RspecFlaky::Listener, :aggregate_failures do
let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' }
let(:suite_flaky_example_report) do
{
'abc123' => {
already_flaky_example_uid => {
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: instance_of(Time),
last_attempts_count: 2,
last_flaky_at: 4321,
last_attempts_count: 3,
flaky_reports: 1,
last_flaky_job: nil
}
}
end
let(:example_attrs) do
let(:already_flaky_example_attrs) do
{
id: 'spec/foo/bar_spec.rb:2',
metadata: {
file_path: 'spec/foo/bar_spec.rb',
line_number: 2,
full_description: 'hello world'
},
execution_result: double(status: 'passed', exception: nil)
}
end
let(:already_flaky_example) { RspecFlaky::FlakyExample.new(suite_flaky_example_report[already_flaky_example_uid]) }
let(:new_example_attrs) do
{
id: 'spec/foo/baz_spec.rb:3',
metadata: {
......@@ -36,14 +49,14 @@ describe RspecFlaky::Listener do
describe '#initialize' do
shared_examples 'a valid Listener instance' do
let(:expected_all_flaky_examples) { {} }
let(:expected_suite_flaky_examples) { {} }
it 'returns a valid Listener instance' do
listener = described_class.new
expect(listener.to_report(listener.all_flaky_examples))
.to match(hash_including(expected_all_flaky_examples))
expect(listener.new_flaky_examples).to eq({})
expect(listener.to_report(listener.suite_flaky_examples))
.to eq(expected_suite_flaky_examples)
expect(listener.flaky_examples).to eq({})
end
end
......@@ -51,16 +64,16 @@ describe RspecFlaky::Listener do
it_behaves_like 'a valid Listener instance'
end
context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do
context 'when a report file exists and set by SUITE_FLAKY_RSPEC_REPORT_PATH' do
let(:report_file) do
Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
f.write(JSON.pretty_generate(flaky_example_report))
f.write(JSON.pretty_generate(suite_flaky_example_report))
f.rewind
end
end
before do
stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path)
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file.path)
end
after do
......@@ -69,74 +82,122 @@ describe RspecFlaky::Listener do
end
it_behaves_like 'a valid Listener instance' do
let(:expected_all_flaky_examples) { flaky_example_report }
let(:expected_suite_flaky_examples) { suite_flaky_example_report }
end
end
end
describe '#example_passed' do
let(:rspec_example) { double(example_attrs) }
let(:rspec_example) { double(new_example_attrs) }
let(:notification) { double(example: rspec_example) }
let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
shared_examples 'a non-flaky example' do
it 'does not change the flaky examples hash' do
expect { subject.example_passed(notification) }
.not_to change { subject.all_flaky_examples }
expect { listener.example_passed(notification) }
.not_to change { listener.flaky_examples }
end
end
describe 'when the RSpec example does not respond to attempts' do
it_behaves_like 'a non-flaky example'
shared_examples 'an existing flaky example' do
let(:expected_flaky_example) do
{
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
last_attempts_count: 2,
flaky_reports: 2,
last_flaky_job: nil
}
end
describe 'when the RSpec example has 1 attempt' do
let(:rspec_example) { double(example_attrs.merge(attempts: 1)) }
it 'changes the flaky examples hash' do
new_example = RspecFlaky::Example.new(rspec_example)
it_behaves_like 'a non-flaky example'
now = Time.now
Timecop.freeze(now) do
expect { listener.example_passed(notification) }
.to change { listener.flaky_examples[new_example.uid].to_h }
end
describe 'when the RSpec example has 2 attempts' do
let(:rspec_example) { double(example_attrs.merge(attempts: 2)) }
let(:expected_new_flaky_example) do
expect(listener.flaky_examples[new_example.uid].to_h)
.to eq(expected_flaky_example.merge(last_flaky_at: now))
end
end
shared_examples 'a new flaky example' do
let(:expected_flaky_example) do
{
example_id: 'spec/foo/baz_spec.rb:3',
file: 'spec/foo/baz_spec.rb',
line: 3,
description: 'hello GitLab',
first_flaky_at: instance_of(Time),
last_flaky_at: instance_of(Time),
last_attempts_count: 2,
flaky_reports: 1,
last_flaky_job: nil
}
end
it 'does not change the flaky examples hash' do
expect { subject.example_passed(notification) }
.to change { subject.all_flaky_examples }
it 'changes the all flaky examples hash' do
new_example = RspecFlaky::Example.new(rspec_example)
expect(subject.all_flaky_examples[new_example.uid].to_h)
.to match(hash_including(expected_new_flaky_example))
now = Time.now
Timecop.freeze(now) do
expect { listener.example_passed(notification) }
.to change { listener.flaky_examples[new_example.uid].to_h }
end
expect(listener.flaky_examples[new_example.uid].to_h)
.to eq(expected_flaky_example.merge(first_flaky_at: now, last_flaky_at: now))
end
end
describe 'when the RSpec example does not respond to attempts' do
it_behaves_like 'a non-flaky example'
end
describe 'when the RSpec example has 1 attempt' do
let(:rspec_example) { double(new_example_attrs.merge(attempts: 1)) }
it_behaves_like 'a non-flaky example'
end
describe 'when the RSpec example has 2 attempts' do
let(:rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
it_behaves_like 'a new flaky example'
context 'with an existing flaky example' do
let(:rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
it_behaves_like 'an existing flaky example'
end
end
end
describe '#dump_summary' do
let(:rspec_example) { double(example_attrs) }
let(:notification) { double(example: rspec_example) }
let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
let(:notification_new_flaky_rspec_example) { double(example: new_flaky_rspec_example) }
let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) }
context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do
context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
let(:new_report_file_path) { Rails.root.join('tmp', 'rspec_flaky_new_report.json') }
before do
stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path)
stub_env('FLAKY_RSPEC_REPORT_PATH', report_file_path)
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', new_report_file_path)
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
after do
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
FileUtils.rm(new_report_file_path) if File.exist?(new_report_file_path)
end
context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
......@@ -144,12 +205,13 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
end
it 'does not write the report file' do
subject.example_passed(notification)
it 'does not write any report file' do
listener.example_passed(notification_new_flaky_rspec_example)
subject.dump_summary(nil)
listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(false)
expect(File.exist?(new_report_file_path)).to be(false)
end
end
......@@ -158,21 +220,39 @@ describe RspecFlaky::Listener do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
end
it 'writes the report file' do
subject.example_passed(notification)
around do |example|
Timecop.freeze { example.run }
end
it 'writes the report files' do
listener.example_passed(notification_new_flaky_rspec_example)
listener.example_passed(notification_already_flaky_rspec_example)
subject.dump_summary(nil)
listener.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(true)
expect(File.exist?(new_report_file_path)).to be(true)
expect(File.read(report_file_path))
.to eq(JSON.pretty_generate(listener.to_report(listener.flaky_examples)))
new_example = RspecFlaky::Example.new(notification_new_flaky_rspec_example)
new_flaky_example = RspecFlaky::FlakyExample.new(new_example)
new_flaky_example.update_flakiness!
expect(File.read(new_report_file_path))
.to eq(JSON.pretty_generate(listener.to_report(new_example.uid => new_flaky_example)))
end
end
end
end
describe '#to_report' do
let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
it 'transforms the internal hash to a JSON-ready hash' do
expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123'])))
.to match(hash_including(flaky_example_report))
expect(listener.to_report(already_flaky_example_uid => already_flaky_example))
.to match(hash_including(suite_flaky_example_report))
end
end
end
require 'spec_helper'
describe IssuablePolicy, models: true do
describe '#rules' do
context 'when discussion is locked for the issuable' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, discussion_locked: true) }
let(:policies) { described_class.new(user, issue) }
context 'when the user is not a project member' do
it 'can not create a note' do
expect(policies).to be_disallowed(:create_note)
end
end
context 'when the user is a project member' do
before do
project.add_guest(user)
end
it 'can create a note' do
expect(policies).to be_allowed(:create_note)
end
end
end
end
end
require 'spec_helper'
describe NotePolicy, mdoels: true do
describe '#rules' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
def policies(noteable = nil)
return @policies if @policies
noteable ||= issue
note = create(:note, noteable: noteable, author: user, project: project)
@policies = described_class.new(user, note)
end
context 'when the project is public' do
context 'when the note author is not a project member' do
it 'can edit a note' do
expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
end
end
context 'when the noteable is a snippet' do
it 'can edit note' do
policies = policies(create(:project_snippet, project: project))
expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
end
end
context 'when a discussion is locked' do
before do
issue.update_attribute(:discussion_locked, true)
end
context 'when the note author is a project member' do
before do
project.add_developer(user)
end
it 'can edit a note' do
expect(policies).to be_allowed(:update_note)
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
end
end
context 'when the note author is not a project member' do
it 'can not edit a note' do
expect(policies).to be_disallowed(:update_note)
expect(policies).to be_disallowed(:admin_note)
expect(policies).to be_disallowed(:resolve_note)
end
it 'can read a note' do
expect(policies).to be_allowed(:read_note)
end
end
end
end
end
end
......@@ -302,6 +302,40 @@ describe API::Notes do
expect(private_issue.notes.reload).to be_empty
end
end
context 'when the merge request discussion is locked' do
before do
merge_request.update_attribute(:discussion_locked, true)
end
context 'when a user is a team member' do
subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), body: 'Hi!' }
it 'returns 200 status' do
subject
expect(response).to have_http_status(201)
end
it 'creates a new note' do
expect { subject }.to change { Note.count }.by(1)
end
end
context 'when a user is not a team member' do
subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", private_user), body: 'Hi!' }
it 'returns 403 status' do
subject
expect(response).to have_http_status(403)
end
it 'does not create a new note' do
expect { subject }.not_to change { Note.count }
end
end
end
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
......
require 'spec_helper'
describe Geo::NodeCreateService do
describe '#execute' do
it 'creates a new node with valid params' do
params = { url: 'http://example.com', geo_node_key_attributes: attributes_for(:key) }
service = described_class.new(params)
expect { service.execute }.to change(GeoNode, :count).by(1)
end
it 'does not create a node with invalid params' do
service = described_class.new({ url: 'http://example.com' })
expect { service.execute }.not_to change(GeoNode, :count)
end
it 'returns true when creation succeeds' do
params = { url: 'http://example.com', geo_node_key_attributes: attributes_for(:key) }
service = described_class.new(params)
expect(service.execute).to eq true
end
it 'returns false when creation fails' do
params = { url: 'http://example.com' }
service = described_class.new(params)
expect(service.execute).to eq false
end
it 'parses the namespace_ids when node have namespace restrictions' do
groups = create_list(:group, 2)
params = { url: 'http://example.com', geo_node_key_attributes: attributes_for(:key), namespace_ids: groups.map(&:id).join(',') }
service = described_class.new(params)
service.execute
expect(GeoNode.last.namespace_ids).to match_array(groups.map(&:id))
end
end
end
require 'spec_helper'
describe Geo::NodeUpdateService do
let(:group) { create(:group) }
let(:groups) { create_list(:group, 2) }
let!(:primary) { create(:geo_node, :primary) }
let(:geo_node) { create(:geo_node) }
let(:geo_node_with_restrictions) { create(:geo_node, namespace_ids: [group.id]) }
let(:geo_node_with_restrictions) { create(:geo_node, namespace_ids: [groups.first.id]) }
describe '#execute' do
it 'updates the node without changing the key' do
......@@ -19,6 +19,12 @@ describe Geo::NodeUpdateService do
expect(geo_node.geo_node_key.fingerprint).to eq(original_fingerprint)
end
it 'returns true when update succeeds' do
service = described_class.new(geo_node, { url: 'http://example.com' })
expect(service.execute).to eq true
end
it 'returns false when update fails' do
allow(geo_node).to receive(:update).and_return(false)
......@@ -28,19 +34,19 @@ describe Geo::NodeUpdateService do
end
it 'logs an event to the Geo event log when namespaces change' do
service = described_class.new(geo_node, namespace_ids: [group.id])
service = described_class.new(geo_node, namespace_ids: groups.map(&:id).join(','))
expect { service.execute }.to change(Geo::RepositoriesChangedEvent, :count).by(1)
end
it 'does not log an event to the Geo event log when removing namespace restrictions' do
service = described_class.new(geo_node_with_restrictions, namespace_ids: [])
service = described_class.new(geo_node_with_restrictions, namespace_ids: '')
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
it 'does not log an event to the Geo event log when node is a primary node' do
service = described_class.new(primary, namespace_ids: [group.id])
service = described_class.new(primary, namespace_ids: groups.map(&:id).join(','))
expect { service.execute }.not_to change(Geo::RepositoriesChangedEvent, :count)
end
......
......@@ -48,7 +48,8 @@ describe Issues::UpdateService, :mailer do
assignee_ids: [user2.id],
state_event: 'close',
label_ids: [label.id],
due_date: Date.tomorrow
due_date: Date.tomorrow,
discussion_locked: true
}
end
......@@ -62,6 +63,7 @@ describe Issues::UpdateService, :mailer do
expect(issue).to be_closed
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
expect(issue.discussion_locked).to be_truthy
end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
......@@ -110,6 +112,7 @@ describe Issues::UpdateService, :mailer do
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
expect(issue.discussion_locked).to be_falsey
end
end
......@@ -148,6 +151,13 @@ describe Issues::UpdateService, :mailer do
expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
it 'creates system note about discussion lock' do
note = find_note('locked this issue')
expect(note).not_to be_nil
expect(note.note).to eq 'locked this issue'
end
end
end
......@@ -257,6 +267,30 @@ describe Issues::UpdateService, :mailer do
end
end
context 'when a new assignee added' do
subject { update_issue(assignees: issue.assignees + [user2]) }
it 'creates only 1 new todo' do
expect { subject }.to change { Todo.count }.by(1)
end
it 'creates a todo for new assignee' do
subject
attributes = {
project: project,
author: user,
user: user2,
target_id: issue.id,
target_type: issue.class.name,
action: Todo::ASSIGNED,
state: :pending
}
expect(Todo.where(attributes).count).to eq(1)
end
end
context 'when the milestone change' do
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
......
......@@ -95,7 +95,7 @@ describe MergeRequests::RebaseService do
context 'git commands' do
it 'sets GL_REPOSITORY env variable when calling git commands' do
expect_any_instance_of(described_class)
.to receive(:run_git_command).exactly(4).with(
.to receive(:run_git_command).exactly(3).with(
anything,
anything,
hash_including('GL_REPOSITORY'),
......
......@@ -49,7 +49,8 @@ describe MergeRequests::UpdateService, :mailer do
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
force_remove_source_branch: '1'
force_remove_source_branch: '1',
discussion_locked: true
}
end
......@@ -73,6 +74,7 @@ describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.labels.first.title).to eq(label.name)
expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
expect(@merge_request.discussion_locked).to be_truthy
end
it 'executes hooks with update action' do
......@@ -123,6 +125,13 @@ describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'changed target branch from `master` to `target`'
end
it 'creates system note about discussion lock' do
note = find_note('locked this issue')
expect(note).not_to be_nil
expect(note.note).to eq 'locked this issue'
end
context 'when not including source branch removal options' do
before do
opts.delete(:force_remove_source_branch)
......
......@@ -88,7 +88,10 @@ RSpec.configure do |config|
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = 4
config.reporter.register_listener(RspecFlaky::Listener.new, :example_passed, :dump_summary)
config.reporter.register_listener(
RspecFlaky::Listener.new,
:example_passed,
:dump_summary)
end
config.before(:suite) do
......
module MergeRequestDiffHelpers
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
line[:num].find('.add-diff-note').trigger('click')
end
def get_line_components(line_holder, diff_side = nil)
if diff_side.nil?
get_inline_line_components(line_holder)
else
get_parallel_line_components(line_holder, diff_side)
end
end
def get_inline_line_components(line_holder)
{ content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
end
def get_parallel_line_components(line_holder, diff_side = nil)
side_index = diff_side == 'left' ? 0 : 1
# Wait for `.line_content`
line_holder.find('.line_content', match: :first)
# Wait for `.diff-line-num`
line_holder.find('.diff-line-num', match: :first)
{ content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
end
end
shared_examples 'comment on merge request file' do
it 'adds 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
wait_for_requests
page.within('.notes_holder') do
expect(page).to have_content('Line is wrong')
end
visit(merge_request_path(merge_request))
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 Geo::FileDownloadDispatchWorker do
describe Geo::FileDownloadDispatchWorker, :postgresql do
include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') }
......
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