Commit cf5801ba authored by Luke Bennett's avatar Luke Bennett

Merge branch 'ce-to-ee-2018-01-11' into 'master'

CE upstream - Thursday

Closes #4511, #4552, gitlab-qa#58, gitaly#793, gitlab-ce#41137, and gitlab-ce#41491

See merge request gitlab-org/gitlab-ee!4023
parents 8c0eb08e 4513deb4
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"plugins": [ "plugins": [
["istanbul", { ["istanbul", {
"exclude": [ "exclude": [
"spec/javascripts/**/*" "spec/javascripts/**/*",
"app/assets/javascripts/locale/**/app.js"
] ]
}], }],
["transform-define", { ["transform-define", {
......
...@@ -629,6 +629,7 @@ codequality: ...@@ -629,6 +629,7 @@ codequality:
paths: [codeclimate.json] paths: [codeclimate.json]
sast: sast:
<<: *except-docs
image: registry.gitlab.com/gitlab-org/gl-sast:latest image: registry.gitlab.com/gitlab-org/gl-sast:latest
before_script: [] before_script: []
script: script:
......
app/assets/images/multi-editor-on.png

5.34 KB | W: | H:

app/assets/images/multi-editor-on.png

3.88 KB | W: | H:

app/assets/images/multi-editor-on.png
app/assets/images/multi-editor-on.png
app/assets/images/multi-editor-on.png
app/assets/images/multi-editor-on.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -43,8 +43,7 @@ ...@@ -43,8 +43,7 @@
</h5> </h5>
<a <a
:href="mergeRequest.url" :href="mergeRequest.url"
class="issue-link" class="issue-link">
>
!{{ mergeRequest.iid }} !{{ mergeRequest.iid }}
</a> </a>
&middot; &middot;
...@@ -52,8 +51,7 @@ ...@@ -52,8 +51,7 @@
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a <a
:href="mergeRequest.url" :href="mergeRequest.url"
class="issue-date" class="issue-date">
>
{{ mergeRequest.createdAt }} {{ mergeRequest.createdAt }}
</a> </a>
</span> </span>
...@@ -61,8 +59,7 @@ ...@@ -61,8 +59,7 @@
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
<a <a
:href="mergeRequest.author.webUrl" :href="mergeRequest.author.webUrl"
class="issue-author-link" class="issue-author-link">
>
{{ mergeRequest.author.name }} {{ mergeRequest.author.name }}
</a> </a>
</span> </span>
......
...@@ -47,18 +47,14 @@ ...@@ -47,18 +47,14 @@
<a <a
:href="issue.url" :href="issue.url"
class="issue-link" class="issue-link"
> >#{{ issue.iid }}</a>
#{{ issue.iid }}
</a>
&middot; &middot;
<span> <span>
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
<a <a
:href="issue.url" :href="issue.url"
class="issue-date" class="issue-date"
> >{{ issue.createdAt }}</a>
{{ issue.createdAt }}
</a>
</span> </span>
<span> <span>
{{ s__('ByAuthor|by') }} {{ s__('ByAuthor|by') }}
......
...@@ -57,9 +57,7 @@ ...@@ -57,9 +57,7 @@
<a <a
:href="commit.commitUrl" :href="commit.commitUrl"
class="commit-hash-link commit-sha" class="commit-hash-link commit-sha"
> >{{ commit.shortSha }}</a>
{{ commit.shortSha }}
</a>
{{ s__('FirstPushedBy|pushed by') }} {{ s__('FirstPushedBy|pushed by') }}
<a <a
:href="commit.author.webUrl" :href="commit.author.webUrl"
......
...@@ -46,9 +46,7 @@ ...@@ -46,9 +46,7 @@
<a <a
:href="mergeRequest.url" :href="mergeRequest.url"
class="issue-link" class="issue-link"
> >!{{ mergeRequest.iid }}</a>
!{{ mergeRequest.iid }}
</a>
&middot; &middot;
<span> <span>
{{ s__('OpenedNDaysAgo|Opened') }} {{ s__('OpenedNDaysAgo|Opened') }}
......
...@@ -63,7 +63,8 @@ ...@@ -63,7 +63,8 @@
</a> </a>
<span <span
class="icon-branch" class="icon-branch"
v-html="iconBranch"> v-html="iconBranch"
>
</span> </span>
<a <a
:href="build.commitUrl" :href="build.commitUrl"
......
...@@ -80,16 +80,14 @@ ...@@ -80,16 +80,14 @@
</span> </span>
<a <a
:href="build.commitUrl" :href="build.commitUrl"
class="commit-sha" class="commit-sha">
>
{{ build.shortSha }} {{ build.shortSha }}
</a> </a>
</h5> </h5>
<span> <span>
<a <a
:href="build.url" :href="build.url"
class="issue-date" class="issue-date">
>
{{ build.date }} {{ build.date }}
</a> </a>
</span> </span>
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
actions: { actions: {
type: Array, type: Array,
...@@ -69,7 +68,8 @@ ...@@ -69,7 +68,8 @@
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</span> </span>
...@@ -78,8 +78,7 @@ ...@@ -78,8 +78,7 @@
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li <li
v-for="(action, i) in actions" v-for="(action, i) in actions"
:key="i" :key="i">
>
<button <button
type="button" type="button"
class="js-manual-action-link no-btn btn" class="js-manual-action-link no-btn btn"
......
...@@ -3,13 +3,12 @@ ...@@ -3,13 +3,12 @@
import { s__ } from '../../locale'; import { s__ } from '../../locale';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
......
<script> <script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
......
<script> <script>
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
* *
* Makes a post request when the button is clicked. * Makes a post request when the button is clicked.
*/ */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
......
<script> <script>
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
*/ */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
......
<script> <script>
/** /**
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
default: '', default: '',
}, },
}, },
data() { data() {
return { return {
terminalIconSvg, terminalIconSvg,
......
...@@ -32,7 +32,6 @@ export default { ...@@ -32,7 +32,6 @@ export default {
default: false, default: false,
}, },
}, },
methods: { methods: {
folderUrl(model) { folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`; return `${window.location.pathname}/folders/${model.folderName}`;
...@@ -88,8 +87,7 @@ export default { ...@@ -88,8 +87,7 @@ export default {
</div> </div>
<template <template
v-for="(model, i) in environments" v-for="(model, i) in environments"
:model="model" :model="model">
>
<div <div
is="environment-item" is="environment-item"
:model="model" :model="model"
...@@ -117,8 +115,7 @@ export default { ...@@ -117,8 +115,7 @@ export default {
> >
<div <div
v-if="model.isLoadingFolderContent" v-if="model.isLoadingFolderContent"
:key="i" :key="i">
>
<loading-icon size="2" /> <loading-icon size="2" />
</div> </div>
......
...@@ -29,7 +29,6 @@ ...@@ -29,7 +29,6 @@
required: true, required: true,
}, },
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
......
<script> <script>
/* global Flash */ /* global Flash */
import { s__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import groupsComponent from './groups.vue'; import groupsComponent from './groups.vue';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
modal,
groupsComponent, groupsComponent,
}, },
props: { props: {
...@@ -32,6 +36,10 @@ export default { ...@@ -32,6 +36,10 @@ export default {
isLoading: true, isLoading: true,
isSearchEmpty: false, isSearchEmpty: false,
searchEmptyMessage: '', searchEmptyMessage: '',
showModal: false,
groupLeaveConfirmationMessage: '',
targetGroup: null,
targetParentGroup: null,
}; };
}, },
computed: { computed: {
...@@ -48,7 +56,7 @@ export default { ...@@ -48,7 +56,7 @@ export default {
eventHub.$on('fetchPage', this.fetchPage); eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren); eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup); eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal);
eventHub.$on('updatePagination', this.updatePagination); eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups); eventHub.$on('updateGroups', this.updateGroups);
}, },
...@@ -58,7 +66,7 @@ export default { ...@@ -58,7 +66,7 @@ export default {
beforeDestroy() { beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage); eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren); eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup); eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal);
eventHub.$off('updatePagination', this.updatePagination); eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups); eventHub.$off('updateGroups', this.updateGroups);
}, },
...@@ -141,14 +149,23 @@ export default { ...@@ -141,14 +149,23 @@ export default {
parentGroup.isOpen = false; parentGroup.isOpen = false;
} }
}, },
leaveGroup(group, parentGroup) { showLeaveGroupModal(group, parentGroup) {
const targetGroup = group; this.targetGroup = group;
targetGroup.isBeingRemoved = true; this.targetParentGroup = parentGroup;
this.service.leaveGroup(targetGroup.leavePath) this.showModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
this.showModal = false;
},
leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
$.scrollTo(0); $.scrollTo(0);
this.store.removeGroup(targetGroup, parentGroup); this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.notice, 'notice'); Flash(res.notice, 'notice');
}) })
.catch((err) => { .catch((err) => {
...@@ -157,7 +174,7 @@ export default { ...@@ -157,7 +174,7 @@ export default {
message = COMMON_STR.LEAVE_FORBIDDEN; message = COMMON_STR.LEAVE_FORBIDDEN;
} }
Flash(message); Flash(message);
targetGroup.isBeingRemoved = false; this.targetGroup.isBeingRemoved = false;
}); });
}, },
updatePagination(headers) { updatePagination(headers) {
...@@ -190,5 +207,14 @@ export default { ...@@ -190,5 +207,14 @@ export default {
:search-empty-message="searchEmptyMessage" :search-empty-message="searchEmptyMessage"
:page-info="pageInfo" :page-info="pageInfo"
/> />
<modal
v-show="showModal"
:primary-button-label="__('Leave')"
kind="warning"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
@cancel="hideLeaveGroupModal"
@submit="leaveGroup"
/>
</div> </div>
</template> </template>
<script> <script>
import { s__ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip';
import tooltip from '~/vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue';
import icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub';
import modal from '~/vue_shared/components/modal.vue'; import { COMMON_STR } from '../constants';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default { export default {
components: { components: {
icon, icon,
modal, },
directives: {
tooltip,
},
props: {
parentGroup: {
type: Object,
required: false,
default: () => ({}),
}, },
directives: { group: {
tooltip, type: Object,
required: true,
}, },
props: { },
parentGroup: { computed: {
type: Object, leaveBtnTitle() {
required: false, return COMMON_STR.LEAVE_BTN_TITLE;
default: () => ({}),
},
group: {
type: Object,
required: true,
},
}, },
data() { editBtnTitle() {
return { return COMMON_STR.EDIT_BTN_TITLE;
modalStatus: false,
};
}, },
computed: { },
leaveBtnTitle() { methods: {
return COMMON_STR.LEAVE_BTN_TITLE; onLeaveGroup() {
}, eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup);
editBtnTitle() {
return COMMON_STR.EDIT_BTN_TITLE;
},
leaveConfirmationMessage() {
return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`);
},
}, },
methods: { },
onLeaveGroup() { };
this.modalStatus = true;
},
leaveGroup() {
this.modalStatus = false;
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
},
},
};
</script> </script>
<template> <template>
...@@ -78,14 +63,5 @@ ...@@ -78,14 +63,5 @@
class="leave-group btn no-expand"> class="leave-group btn no-expand">
<icon name="leave"/> <icon name="leave"/>
</a> </a>
<modal
v-show="modalStatus"
:primary-button-label="__('Leave')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to leave this group?')"
:body="leaveConfirmationMessage"
@submit="leaveGroup"
/>
</div> </div>
</template> </template>
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
</div> </div>
</div> </div>
<div> <div>
<repo-tree :tree-id="branch.treeId"/> <repo-tree :tree-id="branch.treeId" />
</div> </div>
</div> </div>
</template> </template>
...@@ -73,7 +73,8 @@ ...@@ -73,7 +73,8 @@
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
:key="n"> :key="n"
>
<skeleton-loading-container /> <skeleton-loading-container />
</div> </div>
</template> </template>
......
...@@ -61,11 +61,10 @@ ...@@ -61,11 +61,10 @@
v-else v-else
class="vertical-center render-error"> class="vertical-center render-error">
<p class="text-center"> <p class="text-center">
The source could not be displayed because a rendering error occurred. You can The source could not be displayed because a rendering error occurred.
<a You can <a
:href="activeFile.rawPath" :href="activeFile.rawPath"
download download>download</a> it instead.
>download</a> it instead.
</p> </p>
</div> </div>
</div> </div>
......
...@@ -267,6 +267,7 @@ ...@@ -267,6 +267,7 @@
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
......
...@@ -66,7 +66,6 @@ ...@@ -66,7 +66,6 @@
window.addEventListener('resize', this.resizeThrottled, false); window.addEventListener('resize', this.resizeThrottled, false);
} }
}, },
methods: { methods: {
getGraphsData() { getGraphsData() {
this.state = 'loading'; this.state = 'loading';
......
...@@ -259,7 +259,8 @@ ...@@ -259,7 +259,8 @@
<svg <svg
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData"> ref="graphData"
>
<graph-path <graph-path
v-for="(path, index) in timeSeries" v-for="(path, index) in timeSeries"
:key="index" :key="index"
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import Icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
Icon, icon,
}, },
props: { props: {
currentXCoordinate: { currentXCoordinate: {
......
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
return `note_${this.note.id}`; return `note_${this.note.id}`;
}, },
}, },
created() { created() {
eventHub.$on('enterEditMode', ({ noteId }) => { eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) { if (noteId === this.note.id) {
...@@ -59,6 +60,7 @@ ...@@ -59,6 +60,7 @@
} }
}); });
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'deleteNote', 'deleteNote',
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
tooltipText: { tooltipText: {
type: String, type: String,
......
...@@ -63,7 +63,8 @@ ...@@ -63,7 +63,8 @@
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) $(this.$el
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => { .on('click', (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
......
...@@ -250,7 +250,8 @@ ...@@ -250,7 +250,8 @@
<div <div
class="blank-state-row" class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage"> v-if="shouldRenderNoPipelinesMessage"
>
<div class="blank-state-center"> <div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div> </div>
......
...@@ -162,7 +162,8 @@ ...@@ -162,7 +162,8 @@
<ul <ul
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"> aria-labelledby="stageDropdown"
>
<li <li
:class="dropdownClass" :class="dropdownClass"
......
...@@ -164,7 +164,6 @@ ...@@ -164,7 +164,6 @@
}, },
}, },
}; };
</script> </script>
<template> <template>
......
...@@ -96,7 +96,8 @@ ...@@ -96,7 +96,8 @@
<span <span
v-tooltip v-tooltip
:title="item.revision" :title="item.revision"
data-placement="bottom"> data-placement="bottom"
>
{{ item.shortRevision }} {{ item.shortRevision }}
</span> </span>
</td> </td>
...@@ -121,10 +122,12 @@ ...@@ -121,10 +122,12 @@
:aria-label="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body" data-container="body"
v-tooltip v-tooltip
@click="handleDeleteRegistry(item)"> @click="handleDeleteRegistry(item)"
>
<i <i
class="fa fa-trash" class="fa fa-trash"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
</td> </td>
......
...@@ -101,7 +101,8 @@ ...@@ -101,7 +101,8 @@
<div <div
v-for="participant in visibleParticipants" v-for="participant in visibleParticipants"
:key="participant.id" :key="participant.id"
class="participants-author js-participants-author"> class="participants-author js-participants-author"
>
<a <a
class="author_link" class="author_link"
:href="participant.web_url" :href="participant.web_url"
......
...@@ -28,7 +28,6 @@ export default { ...@@ -28,7 +28,6 @@ export default {
beforeDestroy() { beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription); eventHub.$off('toggleSubscription', this.onToggleSubscription);
}, },
methods: { methods: {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
......
...@@ -126,7 +126,6 @@ ...@@ -126,7 +126,6 @@
<template v-if="pipeline.coverage"> <template v-if="pipeline.coverage">
Coverage {{ pipeline.coverage }}% Coverage {{ pipeline.coverage }}%
</template> </template>
</div> </div>
</template> </template>
</div> </div>
......
...@@ -127,7 +127,8 @@ ...@@ -127,7 +127,8 @@
v-if="action.type === 'link'" v-if="action.type === 'link'"
:href="action.path" :href="action.path"
:class="action.cssClass" :class="action.cssClass"
:key="i"> :key="i"
>
{{ action.label }} {{ action.label }}
</a> </a>
......
...@@ -121,7 +121,8 @@ ...@@ -121,7 +121,8 @@
/> />
<div <div
class="md-write-holder" class="md-write-holder"
v-show="!previewMarkdown"> v-show="!previewMarkdown"
>
<div class="zen-backdrop"> <div class="zen-backdrop">
<slot name="textarea"></slot> <slot name="textarea"></slot>
<a <a
......
...@@ -71,13 +71,15 @@ ...@@ -71,13 +71,15 @@
class="js-preview-link" class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
tabindex="-1" tabindex="-1"
@click.prevent="previewMarkdownTab($event)"> @click.prevent="previewMarkdownTab($event)"
>
Preview Preview
</a> </a>
</li> </li>
<li <li
class="md-header-toolbar" class="md-header-toolbar"
:class="{ active: !previewMarkdown }"> :class="{ active: !previewMarkdown }"
>
<toolbar-button <toolbar-button
tag="**" tag="**"
button-title="Add bold text" button-title="Add bold text"
...@@ -125,7 +127,8 @@ ...@@ -125,7 +127,8 @@
data-container="body" data-container="body"
tabindex="-1" tabindex="-1"
title="Go full screen" title="Go full screen"
type="button"> type="button"
>
<icon <icon
name="screen-full" name="screen-full"
/> />
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
return this.quickActionsDocsPath !== ''; return this.quickActionsDocsPath !== '';
}, },
}, },
}; };
</script> </script>
...@@ -27,7 +26,8 @@ ...@@ -27,7 +26,8 @@
<a <a
:href="markdownDocsPath" :href="markdownDocsPath"
target="_blank" target="_blank"
tabindex="-1"> tabindex="-1"
>
Markdown is supported Markdown is supported
</a> </a>
</template> </template>
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
export default { export default {
name: 'Modal', name: 'Modal',
......
<script> <script>
import modal from './modal.vue'; import modal from './modal.vue';
export default { export default {
name: 'RecaptchaModal', name: 'RecaptchaModal',
components: { components: {
modal, modal,
}, },
props: { props: {
html: { html: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
},
}, },
},
data() { data() {
return { return {
script: {}, script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js', scriptSrc: 'https://www.google.com/recaptcha/api.js',
}; };
}, },
watch: { watch: {
html() { html() {
this.appendRecaptchaScript(); this.appendRecaptchaScript();
},
}, },
},
mounted() { mounted() {
window.recaptchaDialogCallback = this.submit.bind(this); window.recaptchaDialogCallback = this.submit.bind(this);
}, },
methods: { methods: {
appendRecaptchaScript() { appendRecaptchaScript() {
this.removeRecaptchaScript(); this.removeRecaptchaScript();
const script = document.createElement('script'); const script = document.createElement('script');
script.src = this.scriptSrc; script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script'); script.classList.add('js-recaptcha-script');
script.async = true; script.async = true;
script.defer = true; script.defer = true;
this.script = script; this.script = script;
document.body.appendChild(script); document.body.appendChild(script);
}, },
removeRecaptchaScript() { removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove(); if (this.script instanceof Element) this.script.remove();
}, },
close() { close() {
this.removeRecaptchaScript(); this.removeRecaptchaScript();
this.$emit('close'); this.$emit('close');
}, },
submit() { submit() {
this.$el.querySelector('form').submit(); this.$el.querySelector('form').submit();
},
}, },
}, };
};
</script> </script>
<template> <template>
......
...@@ -18,14 +18,9 @@ ...@@ -18,14 +18,9 @@
margin: $gl-padding 0; margin: $gl-padding 0;
&.limited-width-container .file-content { &.limited-width-container .file-content {
max-width: $limited-layout-width-sm; max-width: $limited-layout-width;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (min-width: $screen-md-min) {
padding-top: 64px;
padding-bottom: 64px;
}
} }
} }
...@@ -128,7 +123,7 @@ ...@@ -128,7 +123,7 @@
} }
&.wiki { &.wiki {
padding: 30px $gl-padding; padding: $gl-padding;
} }
&.blob-no-preview { &.blob-no-preview {
......
...@@ -16,12 +16,6 @@ ...@@ -16,12 +16,6 @@
display: inline-block; display: inline-block;
} }
@media (min-width: $screen-md-min) {
.blob-viewer[data-type="rich"] {
margin: 20px;
}
}
.ide-view { .ide-view {
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
......
class Projects::Clusters::GcpController < Projects::ApplicationController class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :authorize_google_api, except: [:login] before_action :authorize_google_api, except: [:login]
before_action :authorize_google_project_billing, only: [:new] before_action :authorize_google_project_billing, only: [:new, :create]
before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create]
before_action :verify_billing, only: [:create]
def login def login
begin begin
...@@ -23,24 +24,34 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -23,24 +24,34 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
def create def create
@cluster = ::Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
render :new
end
end
private
def verify_billing
case google_project_billing_status case google_project_billing_status
when 'true' when 'true'
@cluster = ::Clusters::CreateService return
.new(project, current_user, create_params)
.execute(token_in_session)
return redirect_to project_cluster_path(project, @cluster) if @cluster.persisted?
when 'false' when 'false'
flash[:error] = _('Please enable billing for one of your projects to be able to create a cluster.') flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else else
flash[:error] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end end
@cluster = ::Clusters::Cluster.new(create_params)
render :new render :new
end end
private
def create_params def create_params
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
......
...@@ -46,7 +46,7 @@ module BlobHelper ...@@ -46,7 +46,7 @@ module BlobHelper
end end
def ide_edit_text def ide_edit_text
"#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe "#{_('Web IDE')}"
end end
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
......
...@@ -49,6 +49,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -49,6 +49,7 @@ class MergeRequestDiff < ActiveRecord::Base
ensure_commit_shas ensure_commit_shas
save_commits save_commits
save_diffs save_diffs
save
keep_around_commits keep_around_commits
end end
...@@ -56,7 +57,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -56,7 +57,6 @@ class MergeRequestDiff < ActiveRecord::Base
self.start_commit_sha ||= merge_request.target_branch_sha self.start_commit_sha ||= merge_request.target_branch_sha
self.head_commit_sha ||= merge_request.source_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha
self.base_commit_sha ||= find_base_sha self.base_commit_sha ||= find_base_sha
save
end end
# Override head_commit_sha to keep compatibility with merge request diff # Override head_commit_sha to keep compatibility with merge request diff
...@@ -195,7 +195,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -195,7 +195,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def commits_count def commits_count
merge_request_diff_commits.size super || merge_request_diff_commits.size
end end
private private
...@@ -264,13 +264,16 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -264,13 +264,16 @@ class MergeRequestDiff < ActiveRecord::Base
new_attributes[:state] = :overflow if diff_collection.overflow? new_attributes[:state] = :overflow if diff_collection.overflow?
end end
update(new_attributes) assign_attributes(new_attributes)
end end
def save_commits def save_commits
MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse) MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse)
merge_request_diff_commits.reload # merge_request_diff_commits.reload is preferred way to reload associated
# objects but it returns cached result for some reason in this case
commits = merge_request_diff_commits(true)
self.commits_count = commits.size
end end
def repository def repository
......
...@@ -1161,7 +1161,7 @@ class Project < ActiveRecord::Base ...@@ -1161,7 +1161,7 @@ class Project < ActiveRecord::Base
def change_head(branch) def change_head(branch)
if repository.branch_exists?(branch) if repository.branch_exists?(branch)
repository.before_change_head repository.before_change_head
repository.write_ref('HEAD', "refs/heads/#{branch}") repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.after_change_head repository.after_change_head
reload_default_branch reload_default_branch
......
...@@ -267,7 +267,7 @@ class Repository ...@@ -267,7 +267,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
begin begin
write_ref(keep_around_ref_name(sha), sha) raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Rugged::ReferenceError => ex rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex rescue Rugged::OSError => ex
...@@ -281,10 +281,6 @@ class Repository ...@@ -281,10 +281,6 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.commit(root_ref).id root_ref_hash = raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
......
...@@ -2,7 +2,10 @@ class CheckGcpProjectBillingService ...@@ -2,7 +2,10 @@ class CheckGcpProjectBillingService
def execute(token) def execute(token)
client = GoogleApi::CloudPlatform::Client.new(token, nil) client = GoogleApi::CloudPlatform::Client.new(token, nil)
client.projects_list.select do |project| client.projects_list.select do |project|
client.projects_get_billing_info(project.name).billingEnabled begin
client.projects_get_billing_info(project.project_id).billing_enabled
rescue
end
end end
end end
end end
...@@ -24,7 +24,7 @@ module Issues ...@@ -24,7 +24,7 @@ module Issues
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes rewrite_notes
rewrite_award_emoji rewrite_issue_award_emoji
add_note_moved_from add_note_moved_from
# Old issue tasks # Old issue tasks
...@@ -76,7 +76,7 @@ module Issues ...@@ -76,7 +76,7 @@ module Issues
end end
def rewrite_notes def rewrite_notes
@old_issue.notes.find_each do |note| @old_issue.notes_with_associations.find_each do |note|
new_note = note.dup new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue, new_params = { project: @new_project, noteable: @new_issue,
note: rewrite_content(new_note.note), note: rewrite_content(new_note.note),
...@@ -84,13 +84,19 @@ module Issues ...@@ -84,13 +84,19 @@ module Issues
updated_at: note.updated_at } updated_at: note.updated_at }
new_note.update(new_params) new_note.update(new_params)
rewrite_award_emoji(note, new_note)
end end
end end
def rewrite_award_emoji def rewrite_issue_award_emoji
@old_issue.award_emoji.each do |award| rewrite_award_emoji(@old_issue, @new_issue)
end
def rewrite_award_emoji(old_awardable, new_awardable)
old_awardable.award_emoji.each do |award|
new_award = award.dup new_award = award.dup
new_award.awardable = @new_issue new_award.awardable = new_awardable
new_award.save new_award.save
end end
end end
......
...@@ -156,13 +156,9 @@ module MergeRequests ...@@ -156,13 +156,9 @@ module MergeRequests
end end
def assign_title_from_issue def assign_title_from_issue
return unless issue return unless issue && issue.is_a?(Issue)
merge_request.title = merge_request.title = "Resolve \"#{issue.title}\""
case issue
when Issue then "Resolve \"#{issue.title}\""
when ExternalIssue then "Resolve #{issue.title}"
end
end end
def issue_iid def issue_iid
......
module MergeRequests module MergeRequests
class RebaseService < MergeRequests::WorkingCopyBaseService class RebaseService < MergeRequests::WorkingCopyBaseService
REBASE_ERROR = 'Rebase failed. Please rebase locally'.freeze
def execute(merge_request) def execute(merge_request)
@merge_request = merge_request @merge_request = merge_request
if rebase if rebase
success success
else else
error('Failed to rebase. Should be done manually') error(REBASE_ERROR)
end end
end end
...@@ -22,8 +24,8 @@ module MergeRequests ...@@ -22,8 +24,8 @@ module MergeRequests
true true
rescue => e rescue => e
log_error('Failed to rebase branch:') log_error(REBASE_ERROR, save_message_on_model: true)
log_error(e.message, save_message_on_model: true) log_error(e.message)
false false
end end
end end
......
...@@ -56,8 +56,6 @@ ...@@ -56,8 +56,6 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li %li
= link_to "Settings", profile_path = link_to "Settings", profile_path
%li
= link_to "Turn on multi edit", profile_preferences_path
- if current_user - if current_user
%li %li
= link_to "Help", help_path = link_to "Help", help_path
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
GitLab multi file editor Web IDE (Beta)
%p Unlock an additional editing experience which makes it possible to edit and commit multiple files %p Enable the new web IDE on this device to make it possible to open and edit multiple files with a single commit
.col-lg-8.multi-file-editor-options .col-lg-8.multi-file-editor-options
= label_tag do = label_tag do
.preview.append-bottom-10= image_tag "multi-editor-off.png" .preview.append-bottom-10= image_tag "multi-editor-off.png"
......
...@@ -30,12 +30,13 @@ ...@@ -30,12 +30,13 @@
%li %li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do = link_to project_new_blob_path(@project, @project.default_branch || 'master') do
#{ _('New file') } #{ _('New file') }
%li - unless @project.empty_repo?
= link_to new_project_branch_path(@project) do %li
#{ _('New branch') } = link_to new_project_branch_path(@project) do
%li #{ _('New branch') }
= link_to new_project_tag_path(@project) do %li
#{ _('New tag') } = link_to new_project_tag_path(@project) do
#{ _('New tag') }
- elsif current_user && current_user.already_forked?(@project) - elsif current_user && current_user.already_forked?(@project)
%li %li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do = link_to project_new_blob_path(@project, @project.default_branch || 'master') do
......
...@@ -4,11 +4,11 @@ ...@@ -4,11 +4,11 @@
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul %ul
%li %li
- link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine } = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li %li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements } = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li %li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs "Clusters", project_clusters_path(@project) - add_to_breadcrumbs "Clusters", project_clusters_path(@project)
- breadcrumb_title @cluster.id - breadcrumb_title @cluster.name
- page_title _("Cluster") - page_title _("Cluster")
- expanded = Rails.env.test? - expanded = Rails.env.test?
......
...@@ -35,3 +35,6 @@ ...@@ -35,3 +35,6 @@
- if diff_file.mode_changed? - if diff_file.mode_changed?
%small %small
#{diff_file.a_mode}#{diff_file.b_mode} #{diff_file.a_mode}#{diff_file.b_mode}
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
%span.label.label-lfs.append-right-5 LFS
...@@ -4,7 +4,7 @@ class CheckGcpProjectBillingWorker ...@@ -4,7 +4,7 @@ class CheckGcpProjectBillingWorker
include ApplicationWorker include ApplicationWorker
include ClusterQueue include ClusterQueue
LEASE_TIMEOUT = 15.seconds.to_i LEASE_TIMEOUT = 3.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour BILLING_TIMEOUT = 1.hour
...@@ -23,13 +23,13 @@ class CheckGcpProjectBillingWorker ...@@ -23,13 +23,13 @@ class CheckGcpProjectBillingWorker
end end
def self.redis_shared_state_key_for(token) def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{token.hash}:billing_enabled" "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
end end
def perform(token_key) def perform(token_key)
return unless token_key return unless token_key
token = self.get_session_token(token_key) token = self.class.get_session_token(token_key)
return unless token return unless token
return unless try_obtain_lease_for(token) return unless try_obtain_lease_for(token)
......
---
title: Default merge request title is set correctly again when external issue tracker is activated
merge_request: 16356
author: Ben305
type: fixed
---
title: Store number of commits in merge_request_diffs table.
merge_request:
author:
type: performance
---
title: Add `pipelines` endpoint to merge requests API
merge_request: 15454
author: Tony Rom <thetonyrom@gmail.com>
type: added
---
title: Hide new branch and tag links for projects with an empty repo
merge_request:
author:
type: fixed
---
title: Display user friendly error message if rebase fails.
merge_request:
author:
type: fixed
---
title: Make project README containers wider on fixed layout
merge_request: 16181
author: Takuya Noguchi
type: fixed
---
title: Make modal dialog common for Groups tree app
merge_request: 16311
author:
type: fixed
---
title: Make rich blob viewer wider for PC
merge_request: 16262
author: Takuya Noguchi
type: fixed
---
title: Fix web ide user preferences copy and buttons
merge_request: 41789
author:
type: other
---
title: Add rake task to check integrity of uploaded files
merge_request:
author:
type: added
---
title: Fix bug where award emojis would be lost when moving issues between projects
merge_request:
author:
type: fixed
class AddCommitsCountToMergeRequestDiff < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'AddMergeRequestDiffCommitsCount'.freeze
BATCH_SIZE = 5000
DELAY_INTERVAL = 5.minutes.to_i
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
include ::EachBatch
end
disable_ddl_transaction!
def up
add_column :merge_request_diffs, :commits_count, :integer
say 'Populating the MergeRequestDiff `commits_count`'
queue_background_migration_jobs_by_range_at_intervals(MergeRequestDiff, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
remove_column :merge_request_diffs, :commits_count
end
end
...@@ -1397,6 +1397,7 @@ ActiveRecord::Schema.define(version: 20180105233807) do ...@@ -1397,6 +1397,7 @@ ActiveRecord::Schema.define(version: 20180105233807) do
t.string "real_size" t.string "real_size"
t.string "head_commit_sha" t.string "head_commit_sha"
t.string "start_commit_sha" t.string "start_commit_sha"
t.integer "commits_count"
end end
add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
......
...@@ -66,3 +66,15 @@ On the sign in page there should now be a Crowd tab in the sign in form. ...@@ -66,3 +66,15 @@ On the sign in page there should now be a Crowd tab in the sign in form.
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source [restart]: ../restart_gitlab.md#installations-from-source
## Troubleshooting
If you see an error message like the one below when you sign in after Crowd authentication is configured, you may want to consult the Crowd administrator for the Crowd log file to know the exact cause:
```
could not authorize you from Crowd because invalid credentials
```
Please make sure the Crowd users who need to login to GitLab are authorized to [the application](#configure-a-new-crowd-application) in the step of **Authorisation**. This could be verified by try "Authentication test" for Crowd as of 2.11.
![Example Crowd application authorisation configuration](img/crowd_application_authorisation.png)
\ No newline at end of file
...@@ -76,6 +76,39 @@ Example output: ...@@ -76,6 +76,39 @@ Example output:
![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png) ![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png)
## Uploaded Files Integrity
The uploads check Rake task will loop through all uploads in the database
and run two checks to determine the integrity of each file:
1. Check if the file exist on the file system.
1. Check if the checksum of the file on the file system matches the checksum in the database.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:uploads:check
```
**Source Installation**
```bash
sudo -u git -H bundle exec rake gitlab:uploads:check RAILS_ENV=production
```
This task also accepts some environment variables which you can use to override
certain values:
Variable | Type | Description
-------- | ---- | -----------
`BATCH` | integer | Specifies the size of the batch. Defaults to 200.
`ID_FROM` | integer | Specifies the ID to start from, inclusive of the value.
`ID_TO` | integer | Specifies the ID value to end at, inclusive of the value.
```bash
sudo gitlab-rake gitlab:uploads:check BATCH=100 ID_FROM=50 ID_TO=250
```
## LDAP Check ## LDAP Check
The LDAP check Rake task will test the bind_dn and password credentials The LDAP check Rake task will test the bind_dn and password credentials
......
...@@ -474,6 +474,30 @@ Parameters: ...@@ -474,6 +474,30 @@ Parameters:
} }
``` ```
## List MR pipelines
Get a list of merge request pipelines.
```
GET /projects/:id/merge_requests/:merge_request_iid/pipelines
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
```json
[
{
"id": 77,
"sha": "959e04d7c7a30600c894bd3c0cd0e1ce7f42c11d",
"ref": "master",
"status": "success"
}
]
```
## Create MR ## Create MR
Creates a new merge request. Creates a new merge request.
......
...@@ -368,7 +368,6 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ...@@ -368,7 +368,6 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
// bad // bad
<component <component
bar="bar" /> bar="bar" />
``` ```
#### Quotes #### Quotes
...@@ -520,7 +519,6 @@ On those a default key should not be provided. ...@@ -520,7 +519,6 @@ On those a default key should not be provided.
1. Properties in a Vue Component: 1. Properties in a Vue Component:
Check [order of properties in components rule][vue-order]. Check [order of properties in components rule][vue-order].
#### Vue and Bootstrap #### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
......
...@@ -25,7 +25,7 @@ It is possible to run end-to-end tests (eventually being run within a ...@@ -25,7 +25,7 @@ It is possible to run end-to-end tests (eventually being run within a
the `package-qa` manual action, that should be present in a merge request the `package-qa` manual action, that should be present in a merge request
widget. widget.
Mmanual action that starts end-to-end tests is also available in merge requests Manual action that starts end-to-end tests is also available in merge requests
in Omnibus GitLab project. in Omnibus GitLab project.
Below you can read more about how to use it and how does it work. Below you can read more about how to use it and how does it work.
......
...@@ -228,7 +228,7 @@ instance and project. In addition, all admins can use the admin interface under ...@@ -228,7 +228,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------| |---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ | | See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ | | Retry or cancel job | | ✓ | ✓ | ✓ |
| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ | | Erase job artifacts and trace | | ✓ [^5] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ | | Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ | | Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ | | Change project configuration | | | ✓ | ✓ |
...@@ -251,13 +251,13 @@ users: ...@@ -251,13 +251,13 @@ users:
| Run CI job | | ✓ | ✓ | ✓ | | Run CI job | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ | | Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ | | Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
| Clone source and LFS from internal projects | | ✓ [^5] | ✓ [^5] | ✓ | | Clone source and LFS from internal projects | | ✓ [^6] | ✓ [^6] | ✓ |
| Clone source and LFS from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | | Clone source and LFS from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] |
| Push source and LFS | | | | | | Push source and LFS | | | | |
| Pull container images from current project | | ✓ | ✓ | ✓ | | Pull container images from current project | | ✓ | ✓ | ✓ |
| Pull container images from public projects | | ✓ | ✓ | ✓ | | Pull container images from public projects | | ✓ | ✓ | ✓ |
| Pull container images from internal projects| | ✓ [^5] | ✓ [^5] | ✓ | | Pull container images from internal projects| | ✓ [^6] | ✓ [^6] | ✓ |
| Pull container images from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | | Pull container images from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] |
| Push container images to current project | | ✓ | ✓ | ✓ | | Push container images to current project | | ✓ | ✓ | ✓ |
| Push container images to other projects | | | | | | Push container images to other projects | | | | |
...@@ -287,13 +287,14 @@ with the permissions described on the documentation on [auditor users permission ...@@ -287,13 +287,14 @@ with the permissions described on the documentation on [auditor users permission
Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/)
only. only.
[^1]: On public and internal projects, all users are able to perform this action. [^1]: On public and internal projects, all users are able to perform this action
[^2]: Guest users can only view the confidential issues they created themselves [^2]: Guest users can only view the confidential issues they created themselves
[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD** [^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner [^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one. [^5]: Only if the job was triggered by the user
[^6]: Only if user is a member of the project. [^6]: Only if user is not external one
[^7]: Only if the build was triggered by the user [^7]: Only if user is a member of the project
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md [new-mod]: project/new_ci_build_permissions_model.md
[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998 [ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
......
...@@ -23,9 +23,14 @@ following prerequisites must be met. ...@@ -23,9 +23,14 @@ following prerequisites must be met.
be enabled in GitLab at the instance level. If that's not the case, ask your be enabled in GitLab at the instance level. If that's not the case, ask your
GitLab administrator to enable it. GitLab administrator to enable it.
- Your associated Google account must have the right privileges to manage - Your associated Google account must have the right privileges to manage
clusters on GKE. That would mean that a clusters on GKE. That would mean that a [billing
[billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
must be set up. must be set up and that you have to have permissions to access it.
- You must have Master [permissions] in order to be able to access the
**Cluster** page.
- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled
- You must have [Resource Manager
API](https://cloud.google.com/resource-manager/)
**For an existing Kubernetes cluster:** **For an existing Kubernetes cluster:**
......
...@@ -10,12 +10,7 @@ in the table below. ...@@ -10,12 +10,7 @@ in the table below.
| `description` | A name for the issue tracker (to differentiate between instances, for example) | | `description` | A name for the issue tracker (to differentiate between instances, for example) |
| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | | `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | | `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | | `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** |
Once you have configured and enabled Redmine:
- the **Issues** link on the GitLab project pages takes you to the appropriate
Redmine issue index
- clicking **New issue** on the project dashboard creates a new Redmine issue
As an example, below is a configuration for a project named gitlab-ci. As an example, below is a configuration for a project named gitlab-ci.
......
...@@ -24,6 +24,13 @@ module API ...@@ -24,6 +24,13 @@ module API
.preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs) .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs)
end end
def merge_request_pipelines_with_access
authorize! :read_pipeline, user_project
mr = find_merge_request_with_access(params[:merge_request_iid])
mr.all_pipelines
end
params :merge_requests_params do params :merge_requests_params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all', optional :state, type: String, values: %w[opened closed merged all], default: 'all',
desc: 'Return opened, closed, merged, or all merge requests' desc: 'Return opened, closed, merged, or all merge requests'
...@@ -226,6 +233,15 @@ module API ...@@ -226,6 +233,15 @@ module API
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end end
desc 'Get the merge request pipelines' do
success Entities::PipelineBasic
end
get ':id/merge_requests/:merge_request_iid/pipelines' do
pipelines = merge_request_pipelines_with_access
present paginate(pipelines), with: Entities::PipelineBasic
end
desc 'Update a merge request' do desc 'Update a merge request' do
success Entities::MergeRequest success Entities::MergeRequest
end end
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
# rubocop:disable Metrics/LineLength
module Gitlab
module BackgroundMigration
class AddMergeRequestDiffCommitsCount
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
end
def perform(start_id, stop_id)
Rails.logger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}")
update = '
commits_count = (
SELECT count(*)
FROM merge_request_diff_commits
WHERE merge_request_diffs.id = merge_request_diff_commits.merge_request_diff_id
)'.squish
MergeRequestDiff.where(id: start_id..stop_id).update_all(update)
end
end
end
end
...@@ -1104,14 +1104,27 @@ module Gitlab ...@@ -1104,14 +1104,27 @@ module Gitlab
end end
end end
def write_ref(ref_path, ref) def write_ref(ref_path, ref, old_ref: nil, shell: true)
if shell
shell_write_ref(ref_path, ref, old_ref)
else
rugged_write_ref(ref_path, ref)
end
end
def shell_write_ref(ref_path, ref, old_ref)
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
raise ArgumentError, "invalid old_ref #{old_ref.inspect}" if !old_ref.nil? && old_ref.include?("\x00")
input = "update #{ref_path}\x00#{ref}\x00\x00" input = "update #{ref_path}\x00#{ref}\x00#{old_ref}\x00"
run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) } run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) }
end end
def rugged_write_ref(ref_path, ref)
rugged.references.create(ref_path, ref, force: true)
end
def fetch_ref(source_repository, source_ref:, target_ref:) def fetch_ref(source_repository, source_ref:, target_ref:)
Gitlab::Git.check_namespace!(source_repository) Gitlab::Git.check_namespace!(source_repository)
source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository) source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository)
......
...@@ -47,15 +47,15 @@ module GoogleApi ...@@ -47,15 +47,15 @@ module GoogleApi
service.authorization = access_token service.authorization = access_token
service.fetch_all(items: :projects) do |token| service.fetch_all(items: :projects) do |token|
service.list_projects(page_token: token) service.list_projects(page_token: token, options: user_agent_header)
end end
end end
def projects_get_billing_info(project_name) def projects_get_billing_info(project_id)
service = Google::Apis::CloudbillingV1::CloudbillingService.new service = Google::Apis::CloudbillingV1::CloudbillingService.new
service.authorization = access_token service.authorization = access_token
service.get_project_billing_info("projects/#{project_name}") service.get_project_billing_info("projects/#{project_id}", options: user_agent_header)
end end
def projects_zones_clusters_get(project_id, zone, cluster_id) def projects_zones_clusters_get(project_id, zone, cluster_id)
......
namespace :gitlab do
namespace :uploads do
desc 'GitLab | Uploads | Check integrity of uploaded files'
task check: :environment do
puts 'Checking integrity of uploaded files'
uploads_batches do |batch|
batch.each do |upload|
puts "- Checking file (#{upload.id}): #{upload.absolute_path}".color(:green)
if upload.exist?
check_checksum(upload)
else
puts " * File does not exist on the file system".color(:red)
end
end
end
puts 'Done!'
end
def batch_size
ENV.fetch('BATCH', 200).to_i
end
def calculate_checksum(absolute_path)
Digest::SHA256.file(absolute_path).hexdigest
end
def check_checksum(upload)
checksum = calculate_checksum(upload.absolute_path)
if checksum != upload.checksum
puts " * File checksum (#{checksum}) does not match the one in the database (#{upload.checksum})".color(:red)
end
end
def uploads_batches(&block)
Upload.all.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
yield relation
end
end
end
end
...@@ -27,13 +27,17 @@ following call would login to a local [GDK] instance and run all specs in ...@@ -27,13 +27,17 @@ following call would login to a local [GDK] instance and run all specs in
bin/qa Test::Instance http://localhost:3000 bin/qa Test::Instance http://localhost:3000
``` ```
### Writing tests
1. [Using page objects](qa/page/README.md)
### Running specific tests ### Running specific tests
You can also supply specific tests to run as another parameter. For example, to You can also supply specific tests to run as another parameter. For example, to
test the EE license specs, you can run: run the repository-related specs, you can execute:
``` ```
EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/specs/features/ee bin/qa Test::Instance http://localhost qa/specs/features/repository/
``` ```
Since the arguments would be passed to `rspec`, you could use all `rspec` Since the arguments would be passed to `rspec`, you could use all `rspec`
......
require 'forwardable'
module QA module QA
module Factory module Factory
class Base class Base
extend SingleForwardable
def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes
def fabricate!(*_args) def fabricate!(*_args)
raise NotImplementedError raise NotImplementedError
end end
def self.fabricate!(*args) def self.fabricate!(*args)
Factory::Product.populate!(new) do |factory| new.tap do |factory|
yield factory if block_given? yield factory if block_given?
dependencies.each do |name, signature| dependencies.each do |name, signature|
...@@ -14,19 +21,37 @@ module QA ...@@ -14,19 +21,37 @@ module QA
end end
factory.fabricate!(*args) factory.fabricate!(*args)
return Factory::Product.populate!(self)
end end
end end
def self.dependencies def self.evaluator
@dependencies ||= {} @evaluator ||= Factory::Base::DSL.new(self)
end end
def self.dependency(factory, as:, &block) class DSL
as.tap do |name| attr_reader :dependencies, :attributes
class_eval { attr_accessor name }
def initialize(base)
@base = base
@dependencies = {}
@attributes = {}
end
def dependency(factory, as:, &block)
as.tap do |name|
@base.class_eval { attr_accessor name }
Dependency::Signature.new(factory, block).tap do |signature|
@dependencies.store(name, signature)
end
end
end
Dependency::Signature.new(factory, block).tap do |signature| def product(attribute, &block)
dependencies.store(name, signature) Product::Attribute.new(attribute, block).tap do |signature|
@attributes.store(attribute, signature)
end end
end end
end end
......
...@@ -5,8 +5,9 @@ module QA ...@@ -5,8 +5,9 @@ module QA
class Product class Product
include Capybara::DSL include Capybara::DSL
def initialize(factory) Attribute = Struct.new(:name, :block)
@factory = factory
def initialize
@location = current_url @location = current_url
end end
...@@ -15,11 +16,13 @@ module QA ...@@ -15,11 +16,13 @@ module QA
end end
def self.populate!(factory) def self.populate!(factory)
raise ArgumentError unless block_given? new.tap do |product|
factory.attributes.each_value do |attribute|
yield factory product.instance_exec(&attribute.block).tap do |value|
product.define_singleton_method(attribute.name) { value }
new(factory) end
end
end
end end
end end
end end
......
...@@ -13,6 +13,10 @@ module QA ...@@ -13,6 +13,10 @@ module QA
@description = 'My awesome project' @description = 'My awesome project'
end end
product :name do
Page::Project::Show.act { project_name }
end
def fabricate! def fabricate!
group.visit! group.visit!
......
# Page objects in GitLab QA
In GitLab QA we are using a known pattern, called _Page Objects_.
This means that we have built an abstraction for all GitLab pages that we use
to drive GitLab QA scenarios. Whenever we do something on a page, like filling
in a form, or clicking a button, we do that only through a page object
associated with this area of GitLab.
For example, when GitLab QA test harness signs in into GitLab, it needs to fill
in a user login and user password. In order to do that, we have a class, called
`Page::Main::Login` and `sign_in_using_credentials` methods, that is the only
piece of the code, that has knowledge about `user_login` and `user_password`
fields.
## Why do we need that?
We need page objects, because we need to reduce duplication and avoid problems
whenever someone changes some selectors in GitLab's source code.
Imagine that we have a hundred specs in GitLab QA, and we need to sign into
GitLab each time, before we make assertions. Without a page object one would
need to rely on volatile helpers or invoke Capybara methods directly. Imagine
invoking `fill_in :user_login` in every `*_spec.rb` file / test example.
When someone later changes `t.text_field :login` in the view associated with
this page to `t.text_field :username` it will generate a different field
identifier, what would effectively break all tests.
Because we are using `Page::Main::Login.act { sign_in_using_credentials }`
everywhere, when we want to sign into GitLab, the page object is the single
source of truth, and we will need to update `fill_in :user_login`
to `fill_in :user_username` only in a one place.
## What problems did we have in the past?
We do not run QA tests for every commit, because of performance reasons, and
the time it would take to build packages and test everything.
That is why when someone changes `t.text_field :login` to
`t.text_field :username` in the _new session_ view we won't know about this
change until our GitLab QA nightly pipeline fails, or until someone triggers
`package-qa` action in their merge request.
Obviously such a change would break all tests. We call this problem a _fragile
tests problem_.
In order to make GitLab QA more reliable and robust, we had to solve this
problem by introducing coupling between GitLab CE / EE views and GitLab QA.
## How did we solve fragile tests problem?
Currently, when you add a new `Page::Base` derived class, you will also need to
define all selectors that your page objects depends on.
Whenever you push your code to CE / EE repository, `qa:selectors` sanity test
job is going to be run as a part of a CI pipeline.
This test is going to validate all page objects that we have implemented in
`qa/page` directory. When it fails, you will be notified about missing
or invalid views / selectors definition.
## How to properly implement a page object?
We have built a DSL to define coupling between a page object and GitLab views
it is actually implemented by. See an example below.
```ruby
module Page
module Main
class Login < Page::Base
view 'app/views/devise/passwords/edit.html.haml' do
element :password_field, 'password_field :password'
element :password_confirmation, 'password_field :password_confirmation'
element :change_password_button, 'submit "Change your password"'
end
view 'app/views/devise/sessions/_new_base.html.haml' do
element :login_field, 'text_field :login'
element :passowrd_field, 'password_field :password'
element :sign_in_button, 'submit "Sign in"'
end
# ...
end
end
```
It is possible to use `element` DSL method without value, with a String value
or with a Regexp.
```ruby
view 'app/views/my/view.html.haml' do
# Require `f.submit "Sign in"` to be present in `my/view.html.haml
element :my_button, 'f.submit "Sign in"'
# Match every line in `my/view.html.haml` against
# `/link_to .* "My Profile"/` regexp.
element :profile_link, /link_to .* "My Profile"/
# Implicitly require `.qa-logout-button` CSS class to be present in the view
element :logout_button
end
```
## Where to ask for help?
If you need more information, ask for help on `#qa` channel on Slack (GitLab
Team only).
If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab QA issue tracker.
...@@ -4,11 +4,13 @@ module QA ...@@ -4,11 +4,13 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |project| created_project = Factory::Resource::Project.fabricate! do |project|
project.name = 'awesome-project' project.name = 'awesome-project'
project.description = 'create awesome project test' project.description = 'create awesome project test'
end end
expect(created_project.name).to match /^awesome-project-\h{16}$/
expect(page).to have_content( expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/ /Project \S?awesome-project\S+ was successfully created/
) )
......
describe QA::Factory::Base do describe QA::Factory::Base do
let(:factory) { spy('factory') }
let(:product) { spy('product') }
describe '.fabricate!' do describe '.fabricate!' do
subject { Class.new(described_class) } subject { Class.new(described_class) }
let(:factory) { spy('factory') }
let(:product) { spy('product') }
before do before do
allow(QA::Factory::Product).to receive(:new).and_return(product) allow(QA::Factory::Product).to receive(:new).and_return(product)
...@@ -59,30 +60,63 @@ describe QA::Factory::Base do ...@@ -59,30 +60,63 @@ describe QA::Factory::Base do
it 'defines dependency accessors' do it 'defines dependency accessors' do
expect(subject.new).to respond_to :mydep, :mydep= expect(subject.new).to respond_to :mydep, :mydep=
end end
end
describe 'building dependencies' do describe 'dependencies fabrication' do
let(:dependency) { double('dependency') } let(:dependency) { double('dependency') }
let(:instance) { spy('instance') } let(:instance) { spy('instance') }
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep
end
end
before do
stub_const('Some::MyDependency', dependency)
allow(subject).to receive(:new).and_return(instance)
allow(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new)
end
it 'builds all dependencies first' do
expect(dependency).to receive(:fabricate!).once
subject.fabricate!
end
end
end
describe '.product' do
subject do subject do
Class.new(described_class) do Class.new(described_class) do
dependency Some::MyDependency, as: :mydep product :token do
page.do_something_on_page!
'resulting value'
end
end end
end end
before do it 'appends new product attribute' do
stub_const('Some::MyDependency', dependency) expect(subject.attributes).to be_one
expect(subject.attributes).to have_key(:token)
allow(subject).to receive(:new).and_return(instance)
allow(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new)
end end
it 'builds all dependencies first' do describe 'populating fabrication product with data' do
expect(dependency).to receive(:fabricate!).once let(:page) { spy('page') }
before do
allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(product).to receive(:page).and_return(page)
end
subject.fabricate! it 'populates product after fabrication' do
subject.fabricate!
expect(page).to have_received(:do_something_on_page!)
expect(product.token).to eq 'resulting value'
end
end end
end end
end end
...@@ -3,19 +3,8 @@ describe QA::Factory::Product do ...@@ -3,19 +3,8 @@ describe QA::Factory::Product do
let(:product) { spy('product') } let(:product) { spy('product') }
describe '.populate!' do describe '.populate!' do
it 'instantiates and yields factory' do
expect(described_class).to receive(:new).with(factory)
described_class.populate!(factory) do |instance|
instance.something = 'string'
end
expect(factory).to have_received(:something=).with('string')
end
it 'returns a fabrication product' do it 'returns a fabrication product' do
expect(described_class).to receive(:new) expect(described_class).to receive(:new).and_return(product)
.with(factory).and_return(product)
result = described_class.populate!(factory) do |instance| result = described_class.populate!(factory) do |instance|
instance.something = 'string' instance.something = 'string'
...@@ -23,11 +12,6 @@ describe QA::Factory::Product do ...@@ -23,11 +12,6 @@ describe QA::Factory::Product do
expect(result).to be product expect(result).to be product
end end
it 'raises unless block given' do
expect { described_class.populate!(factory) }
.to raise_error ArgumentError
end
end end
describe '.visit!' do describe '.visit!' do
...@@ -37,8 +21,7 @@ describe QA::Factory::Product do ...@@ -37,8 +21,7 @@ describe QA::Factory::Product do
allow_any_instance_of(described_class) allow_any_instance_of(described_class)
.to receive(:visit).and_return('visited some url') .to receive(:visit).and_return('visited some url')
expect(described_class.new(factory).visit!) expect(subject.visit!).to eq 'visited some url'
.to eq 'visited some url'
end end
end end
end end
...@@ -137,11 +137,14 @@ describe Projects::Clusters::GcpController do ...@@ -137,11 +137,14 @@ describe Projects::Clusters::GcpController do
context 'when access token is valid' do context 'when access token is valid' do
before do before do
stub_google_api_validate_token stub_google_api_validate_token
allow_any_instance_of(described_class).to receive(:authorize_google_project_billing)
end end
context 'when google project billing is enabled' do context 'when google project billing is enabled' do
before do before do
stub_google_project_billing_status redis_double = double
allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
end end
it 'creates a new cluster' do it 'creates a new cluster' do
...@@ -158,7 +161,7 @@ describe Projects::Clusters::GcpController do ...@@ -158,7 +161,7 @@ describe Projects::Clusters::GcpController do
it 'renders the cluster form with an error' do it 'renders the cluster form with an error' do
go go
expect(response).to set_flash[:error] expect(response).to set_flash[:alert]
expect(response).to render_template('new') expect(response).to render_template('new')
end end
end end
......
...@@ -80,8 +80,8 @@ feature 'EE Clusters' do ...@@ -80,8 +80,8 @@ feature 'EE Clusters' do
allow_any_instance_of(Projects::Clusters::GcpController) allow_any_instance_of(Projects::Clusters::GcpController)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
stub_google_project_billing_status
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true')
allow_any_instance_of(GoogleApi::CloudPlatform::Client) allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do .to receive(:projects_zones_clusters_create) do
......
...@@ -8,6 +8,10 @@ describe Geo::RenameRepositoryService do ...@@ -8,6 +8,10 @@ describe Geo::RenameRepositoryService do
subject(:service) { described_class.new(project.id, old_path, new_path) } subject(:service) { described_class.new(project.id, old_path, new_path) }
describe '#execute' do describe '#execute' do
before do
TestEnv.clean_test_path
end
context 'project backed by legacy storage' do context 'project backed by legacy storage' do
it 'moves the project repositories' do it 'moves the project repositories' do
expect_any_instance_of(Geo::MoveRepositoryService).to receive(:execute) expect_any_instance_of(Geo::MoveRepositoryService).to receive(:execute)
......
...@@ -13,6 +13,8 @@ feature 'Gcp Cluster', :js do ...@@ -13,6 +13,8 @@ feature 'Gcp Cluster', :js do
end end
context 'when user has signed with Google' do context 'when user has signed with Google' do
let(:project_id) { 'test-project-1234' }
before do before do
allow_any_instance_of(Projects::Clusters::GcpController) allow_any_instance_of(Projects::Clusters::GcpController)
.to receive(:token_in_session).and_return('token') .to receive(:token_in_session).and_return('token')
...@@ -23,7 +25,7 @@ feature 'Gcp Cluster', :js do ...@@ -23,7 +25,7 @@ feature 'Gcp Cluster', :js do
context 'when user has a GCP project with billing enabled' do context 'when user has a GCP project with billing enabled' do
before do before do
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
stub_google_project_billing_status allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true')
end end
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
...@@ -131,15 +133,41 @@ feature 'Gcp Cluster', :js do ...@@ -131,15 +133,41 @@ feature 'Gcp Cluster', :js do
context 'when user does not have a GCP project with billing enabled' do context 'when user does not have a GCP project with billing enabled' do
before do before do
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('false')
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster' click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees form with error' do
expect(page).to have_content('Please enable billing for one of your projects to be able to create a cluster, then try again.')
end
end
context 'when gcp billing status is not in redis' do
before do
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil)
visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE'
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end end
it 'user sees a check page' do it 'user sees form with error' do
pending 'the frontend still has not been implemented' expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
expect(page).to have_link('Continue')
end end
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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