Commit 16dd5a02 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch 'master' into 'qa-define-selectors-ee'

# Conflicts:
#   qa/qa/page/group/show.rb
parents a600eeb3 f9a09067
...@@ -344,6 +344,7 @@ setup-test-env: ...@@ -344,6 +344,7 @@ setup-test-env:
expire_in: 7d expire_in: 7d
paths: paths:
- tmp/tests - tmp/tests
- config/secrets.yml
rspec-pg geo: *rspec-metadata-pg-geo rspec-pg geo: *rspec-metadata-pg-geo
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.4.2 (2018-01-30)
### Fixed (7 changes)
- Fix Epic issue item reordering to handle different scenarios. !4142
- Fix visually broken admin dashboard until license is added. !4196
- Handle empty event timestamp and larger memory units. !4206
- Use a fixed remote name for Geo mirrors. !4249
- Preserve updated issue order to store when reorder is completed. !4278
- Geo - Fix OPENSSH_EXPECTED_COMMAND in the geo:check rake task.
- Execute group hooks after-commit when moving an issue.
## 10.4.1 (2018-01-24) ## 10.4.1 (2018-01-24)
### Fixed (1 change) ### Fixed (1 change)
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.4.2 (2018-01-30)
### Fixed (6 changes)
- Fix copy/paste on iOS devices due to a bug in webkit. !15804
- Fix missing "allow users to request access" option in public project permissions. !16485
- Fix encoding issue when counting commit count. !16637
- Fixes destination already exists, and some particular service errors on Import/Export error. !16714
- Fix cache clear bug withg using : on Windows. !16740
- Use has_table_privilege for TRIGGER on PostgreSQL.
### Changed (1 change)
- Vendor Auto DevOps template with DAST security checks enabled. !16691
## 10.4.1 (2018-01-24) ## 10.4.1 (2018-01-24)
### Fixed (4 changes) ### Fixed (4 changes)
......
...@@ -337,7 +337,7 @@ group :development, :test do ...@@ -337,7 +337,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized' gem 'rspec-parameterized', require: false
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -329,7 +329,7 @@ GEM ...@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab-markup (1.6.3) gitlab-markup (1.6.3)
gitlab-styles (2.3.1) gitlab-styles (2.3.2)
rubocop (~> 0.51) rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { s__ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import Flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -441,13 +443,15 @@ class AwardsHandler { ...@@ -441,13 +443,15 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) { if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton); this.userAuthored($emojiButton);
} else { } else {
$.post(awardUrl, { axios.post(awardUrl, {
name: emoji, name: emoji,
}, (data) => { })
.then(({ data }) => {
if (data.ok) { if (data.ok) {
callback(); callback();
} }
}).fail(() => new Flash('Something went wrong on our end.')); })
.catch(() => flash(s__('Something went wrong on our end.')));
} }
} }
......
/* global ace */ /* global ace */
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob { export default class EditBlob {
...@@ -56,12 +59,14 @@ export default class EditBlob { ...@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') { if (paneId === '#preview') {
this.$toggleButton.hide(); this.$toggleButton.hide();
return $.post(currentLink.data('preview-url'), { axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(), content: this.editor.getValue(),
}, (response) => { })
currentPane.empty().append(response); .then(({ data }) => {
return currentPane.renderGFM(); currentPane.empty().append(data);
}); currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
} }
this.$toggleButton.show(); this.$toggleButton.show();
......
...@@ -12,6 +12,7 @@ export default class CreateItemDropdown { ...@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {}); this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData; this.getDataOption = options.getData;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
...@@ -30,15 +31,15 @@ export default class CreateItemDropdown { ...@@ -30,15 +31,15 @@ export default class CreateItemDropdown {
filterable: true, filterable: true,
remote: false, remote: false,
search: { search: {
fields: ['title'], fields: ['text'],
}, },
selectable: true, selectable: true,
toggleLabel(selected) { toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel; return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
}, },
fieldName: this.fieldName, fieldName: this.fieldName,
text(item) { text(item) {
return _.escape(item.title); return _.escape(item.text);
}, },
id(item) { id(item) {
return _.escape(item.id); return _.escape(item.id);
...@@ -51,6 +52,11 @@ export default class CreateItemDropdown { ...@@ -51,6 +52,11 @@ export default class CreateItemDropdown {
}); });
} }
clearDropdown() {
this.$dropdownContainer.find('.dropdown-content').html('');
this.$dropdownContainer.find('.dropdown-input-field').val('');
}
bindEvents() { bindEvents() {
this.$createButton.on('click', this.onClickCreateWildcard.bind(this)); this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
} }
...@@ -58,9 +64,13 @@ export default class CreateItemDropdown { ...@@ -58,9 +64,13 @@ export default class CreateItemDropdown {
onClickCreateWildcard(e) { onClickCreateWildcard(e) {
e.preventDefault(); e.preventDefault();
this.refreshData();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData` // Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
} }
getData(term, callback) { getData(term, callback) {
...@@ -79,20 +89,28 @@ export default class CreateItemDropdown { ...@@ -79,20 +89,28 @@ export default class CreateItemDropdown {
}); });
} }
toggleCreateNewButton(item) { createNewItemFromValue(newValue) {
if (item) { if (this.createNewItemFromValueOption) {
this.selectedItem = { return this.createNewItemFromValueOption(newValue);
title: item, }
id: item,
text: item, return {
}; title: newValue,
id: newValue,
text: newValue,
};
}
toggleCreateNewButton(newValue) {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer this.$dropdownContainer
.find('.js-dropdown-create-new-item code') .find('.js-dropdown-create-new-item code')
.text(item); .text(newValue);
} }
this.toggleFooter(!item); this.toggleFooter(!newValue);
} }
toggleFooter(toggleState) { toggleFooter(toggleState) {
......
...@@ -115,9 +115,12 @@ export default { ...@@ -115,9 +115,12 @@ export default {
reordered(event) { reordered(event) {
this.removeDraggingCursor(); this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item); const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', { this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10), issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId, afterId,
beforeId, beforeId,
}); });
......
...@@ -176,7 +176,7 @@ export default { ...@@ -176,7 +176,7 @@ export default {
Flash('An error occurred while fetching issues.'); Flash('An error occurred while fetching issues.');
}); });
}, },
saveIssueOrder({ issueId, beforeId, afterId }) { saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
const issueToReorder = _.find(this.state.relatedIssues, issue => issue.id === issueId); const issueToReorder = _.find(this.state.relatedIssues, issue => issue.id === issueId);
if (issueToReorder) { if (issueToReorder) {
...@@ -184,7 +184,14 @@ export default { ...@@ -184,7 +184,14 @@ export default {
endpoint: issueToReorder.relation_path, endpoint: issueToReorder.relation_path,
move_before_id: beforeId, move_before_id: beforeId,
move_after_id: afterId, move_after_id: afterId,
}).catch(() => { })
.then(res => res.json())
.then((res) => {
if (!res.message) {
this.store.updateIssueOrder(oldIndex, newIndex);
}
})
.catch(() => {
Flash('An error occurred while reordering issues.'); Flash('An error occurred while reordering issues.');
}); });
} }
......
...@@ -16,6 +16,13 @@ class RelatedIssuesStore { ...@@ -16,6 +16,13 @@ class RelatedIssuesStore {
this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove); this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove);
} }
updateIssueOrder(oldIndex, newIndex) {
if (this.state.relatedIssues.length > 0) {
const updatedIssue = this.state.relatedIssues.splice(oldIndex, 1)[0];
this.state.relatedIssues.splice(newIndex, 0, updatedIssue);
}
}
setPendingReferences(issues) { setPendingReferences(issues) {
this.state.pendingReferences = issues; this.state.pendingReferences = issues;
} }
......
...@@ -76,7 +76,13 @@ ...@@ -76,7 +76,13 @@
.then(data => this.store.storeDeploymentData(data)) .then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')), .catch(() => new Flash('Error getting deployment information.')),
]) ])
.then(() => { this.showEmptyState = false; }) .then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => { this.state = 'unableToConnect'; }); .catch(() => { this.state = 'unableToConnect'; });
}, },
......
...@@ -34,16 +34,23 @@ ...@@ -34,16 +34,23 @@
svgUrl: this.emptyGettingStartedSvgPath, svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring', title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`, of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Configure Prometheus', buttonText: 'Configure Prometheus',
}, },
loading: { loading: {
svgUrl: this.emptyLoadingSvgPath, svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data', title: 'Waiting for performance data',
description: `Creating graphs uses the data from the Prometheus server. description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`, If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation', buttonText: 'View documentation',
}, },
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
buttonText: 'Configure Prometheus',
},
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server', title: 'Unable to connect to Prometheus server',
......
...@@ -84,7 +84,7 @@ export default { ...@@ -84,7 +84,7 @@ export default {
return !this.showLess || (index < this.defaultRenderCount && this.showLess); return !this.showLess || (index < this.defaultRenderCount && this.showLess);
}, },
avatarUrl(user) { avatarUrl(user) {
return user.avatar || user.avatar_url; return user.avatar || user.avatar_url || gon.default_avatar_url;
}, },
assigneeUrl(user) { assigneeUrl(user) {
return `${this.rootPath}${user.username}`; return `${this.rootPath}${user.username}`;
......
...@@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) {
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, username; var avatar, img, listClosingTags, listWithName, listWithUserName, username;
username = user.username ? "@" + user.username : ""; username = user.username ? "@" + user.username : "";
avatar = user.avatar_url ? user.avatar_url : false; avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
let selected = false; let selected = false;
...@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (user.beforeDivider != null) { if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else { } else {
if (avatar) { img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
} }
return ` return `
......
import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
isMakingRequest: false,
};
},
directives: {
tooltip,
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
return !sourceBranchRemoved && canRemoveSourceBranch &&
!this.isMakingRequest && !isRemovingSourceBranch;
},
shouldShowSourceBranchRemoving() {
const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr;
return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest);
},
shouldShowMergedButtons() {
const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath,
cherryPickInForkPath } = this.mr;
return canRevertInCurrentMR || canCherryPickInCurrentMR ||
revertInForkPath || cherryPickInForkPath;
},
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
class="btn btn-close btn-xs"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
title="Revert this merge request in a new merge request">
Revert
</a>
<a
v-else-if="mr.revertInForkPath"
v-tooltip
class="btn btn-close btn-xs"
data-method="post"
:href="mr.revertInForkPath"
title="Revert this merge request in a new merge request">
Revert
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
class="btn btn-default btn-xs"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
title="Cherry-pick this merge request in a new merge request">
Cherry-pick
</a>
<a
v-else-if="mr.cherryPickInForkPath"
v-tooltip
class="btn btn-default btn-xs"
data-method="post"
:href="mr.cherryPickInForkPath"
title="Cherry-pick this merge request in a new merge request">
Cherry-pick
</a>
</div>
<section class="mr-info-list">
<p>
The changes were merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</p>
<p v-if="mr.sourceBranchRemoved">The source branch has been removed</p>
<p v-if="shouldShowRemoveSourceBranch" class="space-children">
<span>You can remove source branch now</span>
<button
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
class="btn btn-xs btn-default js-remove-branch-button">
Remove Source Branch
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<loading-icon inline />
<span>The source branch is being removed</span>
</p>
</section>
</div>
</div>
`,
};
<script>
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
directives: {
tooltip,
},
components: {
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
},
data() {
return {
isMakingRequest: false,
};
},
computed: {
shouldShowRemoveSourceBranch() {
const {
sourceBranchRemoved,
isRemovingSourceBranch,
canRemoveSourceBranch,
} = this.mr;
return !sourceBranchRemoved &&
canRemoveSourceBranch &&
!this.isMakingRequest &&
!isRemovingSourceBranch;
},
shouldShowSourceBranchRemoving() {
const {
sourceBranchRemoved,
isRemovingSourceBranch,
} = this.mr;
return !sourceBranchRemoved &&
(isRemovingSourceBranch || this.isMakingRequest);
},
shouldShowMergedButtons() {
const {
canRevertInCurrentMR,
canCherryPickInCurrentMR,
revertInForkPath,
cherryPickInForkPath,
} = this.mr;
return canRevertInCurrentMR ||
canCherryPickInCurrentMR ||
revertInForkPath ||
cherryPickInForkPath;
},
revertTitle() {
return s__('mrWidget|Revert this merge request in a new merge request');
},
cherryPickTitle() {
return s__('mrWidget|Cherry-pick this merge request in a new merge request');
},
revertLabel() {
return s__('mrWidget|Revert');
},
cherryPickLabel() {
return s__('mrWidget|Cherry-pick');
},
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
Flash(__('Something went wrong. Please try again.'));
});
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-time
:action-text="s__('mrWidget|Merged by')"
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt"
/>
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
class="btn btn-close btn-xs"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
:title="revertTitle"
>
{{ revertLabel }}
</a>
<a
v-else-if="mr.revertInForkPath"
v-tooltip
class="btn btn-close btn-xs"
data-method="post"
:href="mr.revertInForkPath"
:title="revertTitle"
>
{{ revertLabel }}
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
class="btn btn-default btn-xs"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
:title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
<a
v-else-if="mr.cherryPickInForkPath"
v-tooltip
class="btn btn-default btn-xs"
data-method="post"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
</div>
<section class="mr-info-list">
<p>
{{ s__("mrWidget|The changes were merged into") }}
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
</p>
<p
v-if="shouldShowRemoveSourceBranch"
class="space-children"
>
<span>{{ s__("mrWidget|You can remove source branch now") }}</span>
<button
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
class="btn btn-xs btn-default js-remove-branch-button"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<loading-icon :inline="true" />
<span>
{{ s__("mrWidget|The source branch is being removed") }}
</span>
</p>
</section>
</div>
</div>
</template>
...@@ -16,7 +16,7 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; ...@@ -16,7 +16,7 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged'; export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
......
...@@ -89,7 +89,7 @@ module ApplicationHelper ...@@ -89,7 +89,7 @@ module ApplicationHelper
end end
def default_avatar def default_avatar
'no_avatar.png' asset_path('no_avatar.png')
end end
def last_commit(project) def last_commit(project)
......
...@@ -476,7 +476,7 @@ module Ci ...@@ -476,7 +476,7 @@ module Ci
if cache && project.jobs_cache_index if cache && project.jobs_cache_index
cache = cache.merge( cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}") key: "#{cache[:key]}_#{project.jobs_cache_index}")
end end
[cache] [cache]
......
...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService ...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username, username: self.username,
password: self.password, password: self.password,
site: URI.join(url, '/').to_s, site: URI.join(url, '/').to_s,
context_path: url.path, context_path: url.path.chomp('/'),
auth_type: :basic, auth_type: :basic,
read_timeout: 120, read_timeout: 120,
use_cookies: true, use_cookies: true,
......
...@@ -9,7 +9,8 @@ module MergeRequests ...@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_upon_missing_source_branch_ref
post_merge_manually_merged
reload_merge_requests reload_merge_requests
reset_merge_when_pipeline_succeeds reset_merge_when_pipeline_succeeds
mark_pending_todos_done mark_pending_todos_done
...@@ -30,11 +31,22 @@ module MergeRequests ...@@ -30,11 +31,22 @@ module MergeRequests
private private
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
# the latest diff state as the last _valid_ one.
merge_requests_for_source_branch.reject(&:source_branch_exists?).each do |mr|
MergeRequests::CloseService
.new(mr.target_project, @current_user)
.execute(mr)
end
end
# Collect open merge requests that target same branch we push into # Collect open merge requests that target same branch we push into
# and close if push to master include last commit from merge request # and close if push to master include last commit from merge request
# We need this to close(as merged) merge requests that were merged into # We need this to close(as merged) merge requests that were merged into
# target branch manually # target branch manually
def close_merge_requests def post_merge_manually_merged
commit_ids = @commits.map(&:id) commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select(&:diff_head_commit)
......
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= sprite_icon('plus-square', size: 16) = sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
......
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
.pull-right .pull-right
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
#{ _('Create merge request') } #{ _('Create merge request') }
...@@ -32,5 +32,5 @@ ...@@ -32,5 +32,5 @@
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea', classes: 'note-textarea qa-issuable-form-description',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_quick_actions: supports_quick_actions supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
%span.append-right-10 %span.append-right-10
- if issuable.new_record? - if issuable.new_record?
= form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create qa-issuable-create-button'
- else - else
= form.submit 'Save changes', class: 'btn btn-save' = form.submit 'Save changes', class: 'btn btn-save'
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%div{ class: div_class } %div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true, = form.text_field :title, required: true, maxlength: 255, autofocus: true,
autocomplete: 'off', class: 'form-control pad' autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
- if issuable.respond_to?(:work_in_progress?) - if issuable.respond_to?(:work_in_progress?)
%p.help-block %p.help-block
......
require_relative "../lib/gitlab/upgrader"
Gitlab::Upgrader.new.execute
---
title: Update behavior of MR widgets that require pipeline artifacts to allow jobs
with multiple artifacts
merge_request: 4203
author:
type: changed
---
title: Fix Epic issue item reordering to handle different scenarios
merge_request: 4142
author:
type: fixed
---
title: Use a fixed remote name for Geo mirrors
merge_request: 4249
author:
type: fixed
---
title: Allow project to be set up to push to and pull from same mirror
merge_request:
author:
type: fixed
---
title: Fix visually broken admin dashboard until license is added
merge_request: 4196
author:
type: fixed
---
title: Handle empty event timestamp and larger memory units
merge_request: 4206
author:
type: fixed
---
title: Fix copy/paste on iOS devices due to a bug in webkit
merge_request: 15804
author:
type: fixed
---
title: Fix default avatar icon missing when Gravatar is disabled
merge_request: 16681
author: Felix Geyer
type: fixed
---
title: Adds spacing between edit and delete tag btn in tag list
merge_request: 16757
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix missing "allow users to request access" option in public project permissions
merge_request: 16485
author:
type: fixed
---
title: Fix encoding issue when counting commit count
merge_request: 16637
author:
type: fixed
---
title: Fixes destination already exists, and some particular service errors on Import/Export
error
merge_request: 16714
author:
type: fixed
--- ---
title: Geo - Fix OPENSSH_EXPECTED_COMMAND in the geo:check rake task title: Close and do not reload MR diffs when source branch is deleted
merge_request: merge_request:
author: author:
type: fixed type: fixed
--- ---
title: Use has_table_privilege for TRIGGER on PostgreSQL title: Return more consistent values for merge_status on MR APIs
merge_request: merge_request:
author: author:
type: fixed type: fixed
--- ---
title: Execute group hooks after-commit when moving an issue title: Fix JIRA not working when a trailing slash is included
merge_request: merge_request:
author: author:
type: fixed type: fixed
...@@ -8,6 +8,7 @@ require 'elasticsearch/rails/instrumentation' ...@@ -8,6 +8,7 @@ require 'elasticsearch/rails/instrumentation'
module Gitlab module Gitlab
class Application < Rails::Application class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state') require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
......
if defined?(GrapeRouteHelpers) if defined?(GrapeRouteHelpers)
module GrapeRouteHelpers module GrapeRouteHelpers
module AllRoutes
# Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment.
#
# Without the following fix, when two helper methods are the same, but have different arguments
# (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2))
# if the helper method with the least number of arguments is defined first (because the route was defined first)
# then it will shadow the longer route.
#
# The fix is to sort descending by amount of arguments
def decorated_routes
@decorated_routes ||= all_routes
.map { |r| DecoratedRoute.new(r) }
.sort_by { |r| -r.dynamic_path_segments.count }
end
end
class DecoratedRoute class DecoratedRoute
# GrapeRouteHelpers gem tries to parse the versions # GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition. # from a string, not supporting Grape `version` array definition.
......
...@@ -68,7 +68,7 @@ Example response: ...@@ -68,7 +68,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -98,7 +98,7 @@ Example response: ...@@ -98,7 +98,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path ...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path
curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
``` ```
Example response:
```json
{
"file_name": "app/project.rb",
"branch": "master"
}
```
Parameters: Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
......
...@@ -496,7 +496,7 @@ more of the following options: ...@@ -496,7 +496,7 @@ more of the following options:
- `BACKUP=timestamp_of_backup` - Required if more than one backup exists. - `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
Read what the [backup timestamp is about](#backup-timestamp). Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Do not ask if the authorized_keys file should get regenerated. - `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
### Restore for installation from source ### Restore for installation from source
......
...@@ -38,10 +38,7 @@ In order for the report to show in the merge request, you need to specify a ...@@ -38,10 +38,7 @@ In order for the report to show in the merge request, you need to specify a
`codeclimate.json` as an artifact. GitLab will then check this file and show `codeclimate.json` as an artifact. GitLab will then check this file and show
the information inside the merge request. the information inside the merge request.
`codeclimate.json` needs to be the only artifact file for the job. If you try >**Note:**
to also include other files, like Code Climate's HTML report, it will break the
Code Climate display in the merge request.
If the Code Climate report doesn't have anything to compare to, no information If the Code Climate report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the will be displayed in the merge request area. That is the case when you add the
`codequality` job in your `.gitlab-ci.yml` for the very first time. `codequality` job in your `.gitlab-ci.yml` for the very first time.
......
...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def ...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def
branch for your project. You can choose another branch to be your project's branch for your project. You can choose another branch to be your project's
default under your project's **Settings > General**. default under your project's **Settings > General**.
The default branch is the branched affected by the The default branch is the branch affected by the
[issue closing pattern](../../issues/automatic_issue_closing.md), [issue closing pattern](../../issues/automatic_issue_closing.md),
which means that _an issue will be closed when a merge request is merged to which means that _an issue will be closed when a merge request is merged to
the **default branch**_. the **default branch**_.
......
...@@ -63,7 +63,7 @@ module EE ...@@ -63,7 +63,7 @@ module EE
private private
def has_artifact?(name) def has_artifact?(name)
options.dig(:artifacts, :paths) == [name] && options.dig(:artifacts, :paths)&.include?(name) &&
artifacts_metadata? artifacts_metadata?
end end
end end
......
...@@ -334,12 +334,6 @@ module EE ...@@ -334,12 +334,6 @@ module EE
repository.async_remove_remote(::Repository::MIRROR_REMOTE) repository.async_remove_remote(::Repository::MIRROR_REMOTE)
end end
def import_url_availability
if remote_mirrors.find_by(url: import_url)
errors.add(:import_url, 'is already in use by a remote mirror')
end
end
def username_only_import_url def username_only_import_url
bare_url = read_attribute(:import_url) bare_url = read_attribute(:import_url)
return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url) return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url)
......
...@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base ...@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? }
validates :url, addressable_url: true, if: :url_changed? validates :url, addressable_url: true, if: :url_changed?
before_save :set_new_remote_name, if: :mirror_url_changed? before_save :set_new_remote_name, if: :mirror_url_changed?
...@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base ...@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base
end end
end end
def url_availability
return unless project
if project.import_url == url && project.mirror?
errors.add(:url, 'is already in use')
end
end
def reset_fields def reset_fields
update_columns( update_columns(
last_error: nil, last_error: nil,
......
...@@ -575,7 +575,15 @@ module API ...@@ -575,7 +575,15 @@ module API
expose :work_in_progress?, as: :work_in_progress expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :merge_when_pipeline_succeeds expose :merge_when_pipeline_succeeds
expose :merge_status
# Ideally we should deprecate `MergeRequest#merge_status` exposure and
# use `MergeRequest#mergeable?` instead (boolean).
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information.
expose :merge_status do |merge_request|
merge_request.check_if_can_be_merged
merge_request.merge_status
end
expose :diff_head_sha, as: :sha expose :diff_head_sha, as: :sha
expose :merge_commit_sha expose :merge_commit_sha
expose :user_notes_count expose :user_notes_count
......
...@@ -468,9 +468,13 @@ module Gitlab ...@@ -468,9 +468,13 @@ module Gitlab
} }
options = default_options.merge(options) options = default_options.merge(options)
options[:limit] ||= 0
options[:offset] ||= 0 options[:offset] ||= 0
limit = options[:limit]
if limit == 0 || !limit.is_a?(Integer)
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
gitaly_migrate(:find_commits) do |is_enabled| gitaly_migrate(:find_commits) do |is_enabled|
if is_enabled if is_enabled
gitaly_commit_client.find_commits(options) gitaly_commit_client.find_commits(options)
......
...@@ -5,7 +5,17 @@ module Gitlab ...@@ -5,7 +5,17 @@ module Gitlab
module Popen module Popen
extend self extend self
def popen(cmd, path = nil, vars = {}) Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status]
def popen(cmd, path = nil, vars = {}, &block)
result = popen_with_detail(cmd, path, vars, &block)
[result.stdout << result.stderr, result.status&.exitstatus]
end
# Returns Result
def popen_with_detail(cmd, path = nil, vars = {})
unless cmd.is_a?(Array) unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings" raise "System commands must be given as an array of strings"
end end
...@@ -18,18 +28,21 @@ module Gitlab ...@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path) FileUtils.mkdir_p(path)
end end
cmd_output = "" cmd_stdout = ''
cmd_status = 0 cmd_stderr = ''
cmd_status = nil
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
cmd_output << stdout.read cmd_stdout = stdout.read
cmd_output << stderr.read cmd_stderr = stderr.read
cmd_status = wait_thr.value.exitstatus cmd_status = wait_thr.value
end end
[cmd_output, cmd_status] Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
end end
end end
end end
module Gitlab
module Popen
class Runner
attr_reader :results
def initialize
@results = []
end
def run(commands, &block)
commands.each do |cmd|
# yield doesn't support blocks, so we need to use a block variable
block.call(cmd) do # rubocop:disable Performance/RedundantBlockCall
cmd_result = Gitlab::Popen.popen_with_detail(cmd)
results << cmd_result
cmd_result
end
end
end
def all_success_and_clean?
all_success? && all_stderr_empty?
end
def all_success?
results.all? { |result| result.status.success? }
end
def all_stderr_empty?
results.all? { |result| result.stderr.empty? }
end
def failed_results
results.reject { |result| result.status.success? }
end
def warned_results
results.select do |result|
result.status.success? && !result.stderr.empty?
end
end
end
end
end
# please require all dependencies below: # please require all dependencies below:
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper) require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab module Gitlab
module Redis module Redis
......
require 'rainbow/ext/string' require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize' require 'gitlab/utils/strong_memoize'
# rubocop:disable Rails/Output
module Gitlab module Gitlab
TaskFailedError = Class.new(StandardError) TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError) TaskAbortedByUserError = Class.new(StandardError)
...@@ -96,11 +97,9 @@ module Gitlab ...@@ -96,11 +97,9 @@ module Gitlab
end end
def gid_for(group_name) def gid_for(group_name)
begin Etc.getgrnam(group_name).gid
Etc.getgrnam(group_name).gid rescue ArgumentError # no group
rescue ArgumentError # no group "group #{group_name} doesn't exist"
"group #{group_name} doesn't exist"
end
end end
def gitlab_user def gitlab_user
......
require_relative "popen"
require_relative "version_info"
module Gitlab module Gitlab
class Upgrader class Upgrader
def execute def execute
......
require 'tasks/gitlab/task_helpers'
module SystemCheck module SystemCheck
module Helpers module Helpers
include ::Gitlab::TaskHelpers include ::Gitlab::TaskHelpers
......
desc 'Code duplication analyze via flay' desc 'Code duplication analyze via flay'
task :flay do task :flay do
output = `bundle exec flay --mass 35 app/ lib/gitlab/` output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
if output.include? "Similar code found" if output.include? "Similar code found"
puts output puts output
......
...@@ -4,7 +4,7 @@ namespace :gitlab do ...@@ -4,7 +4,7 @@ namespace :gitlab do
namespace :backup do namespace :backup do
# Create backup of GitLab system # Create backup of GitLab system
desc "GitLab | Create a backup of the GitLab system" desc "GitLab | Create a backup of the GitLab system"
task create: :environment do task create: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
configure_cron_mode configure_cron_mode
...@@ -25,7 +25,7 @@ namespace :gitlab do ...@@ -25,7 +25,7 @@ namespace :gitlab do
# Restore backup of GitLab system # Restore backup of GitLab system
desc 'GitLab | Restore a previously created backup' desc 'GitLab | Restore a previously created backup'
task restore: :environment do task restore: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
configure_cron_mode configure_cron_mode
...@@ -73,7 +73,7 @@ namespace :gitlab do ...@@ -73,7 +73,7 @@ namespace :gitlab do
end end
namespace :repo do namespace :repo do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping repositories ...".color(:blue) $progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories") if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
...@@ -84,7 +84,7 @@ namespace :gitlab do ...@@ -84,7 +84,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring repositories ...".color(:blue) $progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore Backup::Repository.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -92,7 +92,7 @@ namespace :gitlab do ...@@ -92,7 +92,7 @@ namespace :gitlab do
end end
namespace :db do namespace :db do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping database ... ".color(:blue) $progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db") if ENV["SKIP"] && ENV["SKIP"].include?("db")
...@@ -103,7 +103,7 @@ namespace :gitlab do ...@@ -103,7 +103,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring database ... ".color(:blue) $progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore Backup::Database.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -111,7 +111,7 @@ namespace :gitlab do ...@@ -111,7 +111,7 @@ namespace :gitlab do
end end
namespace :builds do namespace :builds do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping builds ... ".color(:blue) $progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds") if ENV["SKIP"] && ENV["SKIP"].include?("builds")
...@@ -122,7 +122,7 @@ namespace :gitlab do ...@@ -122,7 +122,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring builds ... ".color(:blue) $progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore Backup::Builds.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -130,7 +130,7 @@ namespace :gitlab do ...@@ -130,7 +130,7 @@ namespace :gitlab do
end end
namespace :uploads do namespace :uploads do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping uploads ... ".color(:blue) $progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads") if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
...@@ -141,7 +141,7 @@ namespace :gitlab do ...@@ -141,7 +141,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring uploads ... ".color(:blue) $progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore Backup::Uploads.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -149,7 +149,7 @@ namespace :gitlab do ...@@ -149,7 +149,7 @@ namespace :gitlab do
end end
namespace :artifacts do namespace :artifacts do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping artifacts ... ".color(:blue) $progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
...@@ -160,7 +160,7 @@ namespace :gitlab do ...@@ -160,7 +160,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring artifacts ... ".color(:blue) $progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore Backup::Artifacts.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -168,7 +168,7 @@ namespace :gitlab do ...@@ -168,7 +168,7 @@ namespace :gitlab do
end end
namespace :pages do namespace :pages do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping pages ... ".color(:blue) $progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages") if ENV["SKIP"] && ENV["SKIP"].include?("pages")
...@@ -179,7 +179,7 @@ namespace :gitlab do ...@@ -179,7 +179,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring pages ... ".color(:blue) $progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore Backup::Pages.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -187,7 +187,7 @@ namespace :gitlab do ...@@ -187,7 +187,7 @@ namespace :gitlab do
end end
namespace :lfs do namespace :lfs do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping lfs objects ... ".color(:blue) $progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs") if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
...@@ -198,7 +198,7 @@ namespace :gitlab do ...@@ -198,7 +198,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring lfs objects ... ".color(:blue) $progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore Backup::Lfs.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -206,7 +206,7 @@ namespace :gitlab do ...@@ -206,7 +206,7 @@ namespace :gitlab do
end end
namespace :registry do namespace :registry do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping container registry images ... ".color(:blue) $progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
...@@ -221,7 +221,7 @@ namespace :gitlab do ...@@ -221,7 +221,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring container registry images ... ".color(:blue) $progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
......
# Temporary hack, until we migrate all checks to SystemCheck format
require 'system_check'
require 'system_check/helpers'
namespace :gitlab do namespace :gitlab do
desc 'GitLab | Check the configuration of GitLab and its environment' desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check task check: %w{gitlab:gitlab_shell:check
...@@ -12,7 +8,7 @@ namespace :gitlab do ...@@ -12,7 +8,7 @@ namespace :gitlab do
namespace :app do namespace :app do
desc 'GitLab | Check the configuration of the GitLab Rails app' desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [ checks = [
...@@ -46,7 +42,7 @@ namespace :gitlab do ...@@ -46,7 +42,7 @@ namespace :gitlab do
namespace :gitlab_shell do namespace :gitlab_shell do
desc "GitLab | Check the configuration of GitLab Shell" desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
start_checking "GitLab Shell" start_checking "GitLab Shell"
...@@ -254,7 +250,7 @@ namespace :gitlab do ...@@ -254,7 +250,7 @@ namespace :gitlab do
namespace :sidekiq do namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq" desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
start_checking "Sidekiq" start_checking "Sidekiq"
...@@ -313,7 +309,7 @@ namespace :gitlab do ...@@ -313,7 +309,7 @@ namespace :gitlab do
namespace :incoming_email do namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email" desc "GitLab | Check the configuration of Reply by email"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
...@@ -336,7 +332,7 @@ namespace :gitlab do ...@@ -336,7 +332,7 @@ namespace :gitlab do
end end
namespace :ldap do namespace :ldap do
task :check, [:limit] => :environment do |_, args| task :check, [:limit] => :gitlab_environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big. # Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script. # This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100) args.with_defaults(limit: 100)
...@@ -392,7 +388,7 @@ namespace :gitlab do ...@@ -392,7 +388,7 @@ namespace :gitlab do
namespace :repo do namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab" desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do task check: :gitlab_environment do
puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red) puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red)
Rake::Task["gitlab:git:fsck"].execute Rake::Task["gitlab:git:fsck"].execute
end end
...@@ -400,7 +396,7 @@ namespace :gitlab do ...@@ -400,7 +396,7 @@ namespace :gitlab do
namespace :orphans do namespace :orphans do
desc 'Gitlab | Check for orphaned namespaces and repositories' desc 'Gitlab | Check for orphaned namespaces and repositories'
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [ checks = [
SystemCheck::Orphans::NamespaceCheck, SystemCheck::Orphans::NamespaceCheck,
...@@ -411,7 +407,7 @@ namespace :gitlab do ...@@ -411,7 +407,7 @@ namespace :gitlab do
end end
desc 'GitLab | Check for orphaned namespaces in the repositories path' desc 'GitLab | Check for orphaned namespaces in the repositories path'
task check_namespaces: :environment do task check_namespaces: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [SystemCheck::Orphans::NamespaceCheck] checks = [SystemCheck::Orphans::NamespaceCheck]
...@@ -419,7 +415,7 @@ namespace :gitlab do ...@@ -419,7 +415,7 @@ namespace :gitlab do
end end
desc 'GitLab | Check for orphaned repositories in the repositories path' desc 'GitLab | Check for orphaned repositories in the repositories path'
task check_repositories: :environment do task check_repositories: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [SystemCheck::Orphans::RepositoryCheck] checks = [SystemCheck::Orphans::RepositoryCheck]
...@@ -429,7 +425,7 @@ namespace :gitlab do ...@@ -429,7 +425,7 @@ namespace :gitlab do
namespace :user do namespace :user do
desc "GitLab | Check the integrity of a specific user's repositories" desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args| task :check_repos, [:username] => :gitlab_environment do |t, args|
username = args[:username] || prompt("Check repository integrity for username? ".color(:blue)) username = args[:username] || prompt("Check repository integrity for username? ".color(:blue))
user = User.find_by(username: username) user = User.find_by(username: username)
if user if user
......
...@@ -5,7 +5,7 @@ namespace :gitlab do ...@@ -5,7 +5,7 @@ namespace :gitlab do
HASHED_REPOSITORY_NAME = '@hashed'.freeze HASHED_REPOSITORY_NAME = '@hashed'.freeze
desc "GitLab | Cleanup | Clean namespaces" desc "GitLab | Cleanup | Clean namespaces"
task dirs: :environment do task dirs: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
remove_flag = ENV['REMOVE'] remove_flag = ENV['REMOVE']
...@@ -79,7 +79,7 @@ namespace :gitlab do ...@@ -79,7 +79,7 @@ namespace :gitlab do
end end
desc "GitLab | Cleanup | Clean repositories" desc "GitLab | Cleanup | Clean repositories"
task repos: :environment do task repos: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}" move_suffix = "+orphaned+#{Time.now.to_i}"
...@@ -108,7 +108,7 @@ namespace :gitlab do ...@@ -108,7 +108,7 @@ namespace :gitlab do
end end
desc "GitLab | Cleanup | Block users that have been removed in LDAP" desc "GitLab | Cleanup | Block users that have been removed in LDAP"
task block_removed_ldap_users: :environment do task block_removed_ldap_users: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
block_flag = ENV['BLOCK'] block_flag = ENV['BLOCK']
...@@ -139,7 +139,7 @@ namespace :gitlab do ...@@ -139,7 +139,7 @@ namespace :gitlab do
# released. So likely this should only be run once on gitlab.com # released. So likely this should only be run once on gitlab.com
# Faulty refs are moved so they are kept around, else some features break. # Faulty refs are moved so they are kept around, else some features break.
desc 'GitLab | Cleanup | Remove faulty deployment refs' desc 'GitLab | Cleanup | Remove faulty deployment refs'
task move_faulty_deployment_refs: :environment do task move_faulty_deployment_refs: :gitlab_environment do
projects = Project.where(id: Deployment.select(:project_id).distinct) projects = Project.where(id: Deployment.select(:project_id).distinct)
projects.find_each do |project| projects.find_each do |project|
......
namespace :gitlab do namespace :gitlab do
namespace :git do namespace :git do
desc "GitLab | Git | Repack" desc "GitLab | Git | Repack"
task repack: :environment do task repack: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -11,7 +11,7 @@ namespace :gitlab do ...@@ -11,7 +11,7 @@ namespace :gitlab do
end end
desc "GitLab | Git | Run garbage collection on all repos" desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do task gc: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -21,7 +21,7 @@ namespace :gitlab do ...@@ -21,7 +21,7 @@ namespace :gitlab do
end end
desc "GitLab | Git | Prune all repos" desc "GitLab | Git | Prune all repos"
task prune: :environment do task prune: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -31,7 +31,7 @@ namespace :gitlab do ...@@ -31,7 +31,7 @@ namespace :gitlab do
end end
desc 'GitLab | Git | Check all repos integrity' desc 'GitLab | Git | Check all repos integrity'
task fsck: :environment do task fsck: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo| failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo|
check_config_lock(repo) check_config_lock(repo)
check_ref_locks(repo) check_ref_locks(repo)
......
namespace :gitlab do namespace :gitlab do
namespace :gitaly do namespace :gitaly do
desc "GitLab | Install or upgrade gitaly" desc "GitLab | Install or upgrade gitaly"
task :install, [:dir, :repo] => :environment do |t, args| task :install, [:dir, :repo] => :gitlab_environment do |t, args|
require 'toml' require 'toml'
warn_user_is_not_gitlab warn_user_is_not_gitlab
......
require 'tasks/gitlab/task_helpers'
# Prevent StateMachine warnings from outputting during a cron task # Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
namespace :gitlab do task gitlab_environment: :environment do
extend SystemCheck::Helpers extend SystemCheck::Helpers
end end
namespace :gitlab do namespace :gitlab do
namespace :env do namespace :env do
desc "GitLab | Show information about GitLab and its environment" desc "GitLab | Show information about GitLab and its environment"
task info: :environment do task info: :gitlab_environment do
# check if there is an RVM environment # check if there is an RVM environment
rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s) rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version # check Ruby version
......
namespace :gitlab do namespace :gitlab do
desc "GitLab | Setup production application" desc "GitLab | Setup production application"
task setup: :environment do task setup: :gitlab_environment do
setup_db setup_db
end end
......
namespace :gitlab do namespace :gitlab do
namespace :shell do namespace :shell do
desc "GitLab | Install or upgrade gitlab-shell" desc "GitLab | Install or upgrade gitlab-shell"
task :install, [:repo] => :environment do |t, args| task :install, [:repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab warn_user_is_not_gitlab
default_version = Gitlab::Shell.version_required default_version = Gitlab::Shell.version_required
...@@ -58,12 +58,12 @@ namespace :gitlab do ...@@ -58,12 +58,12 @@ namespace :gitlab do
end end
desc "GitLab | Setup gitlab-shell" desc "GitLab | Setup gitlab-shell"
task setup: :environment do task setup: :gitlab_environment do
setup setup
end end
desc "GitLab | Build missing projects" desc "GitLab | Build missing projects"
task build_missing_projects: :environment do task build_missing_projects: :gitlab_environment do
Project.find_each(batch_size: 1000) do |project| Project.find_each(batch_size: 1000) do |project|
path_to_repo = project.repository.path_to_repo path_to_repo = project.repository.path_to_repo
if File.exist?(path_to_repo) if File.exist?(path_to_repo)
...@@ -80,7 +80,7 @@ namespace :gitlab do ...@@ -80,7 +80,7 @@ namespace :gitlab do
end end
desc 'Create or repair repository hooks symlink' desc 'Create or repair repository hooks symlink'
task create_hooks: :environment do task create_hooks: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
puts 'Creating/Repairing hooks symlinks for all repositories' puts 'Creating/Repairing hooks symlinks for all repositories'
......
namespace :gitlab do namespace :gitlab do
namespace :workhorse do namespace :workhorse do
desc "GitLab | Install or upgrade gitlab-workhorse" desc "GitLab | Install or upgrade gitlab-workhorse"
task :install, [:dir, :repo] => :environment do |t, args| task :install, [:dir, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab warn_user_is_not_gitlab
unless args.dir.present? unless args.dir.present?
......
...@@ -2,5 +2,14 @@ unless Rails.env.production? ...@@ -2,5 +2,14 @@ unless Rails.env.production?
require 'haml_lint/rake_task' require 'haml_lint/rake_task'
require 'haml_lint/inline_javascript' require 'haml_lint/inline_javascript'
# Workaround for warnings from parser/current
# TODO: Remove this after we update parser gem
task :haml_lint do
require 'parser'
def Parser.warn(*args)
puts(*args) # static-analysis ignores stdout if status is 0
end
end
HamlLint::RakeTask.new HamlLint::RakeTask.new
end end
require Rails.root.join('lib/gitlab/database')
require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
desc 'GitLab | Sets up PostgreSQL' desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
NamespacesProjectsPathLowerIndexes.new.up NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up AddLowerPathIndexToRoutes.new.up
......
...@@ -27,7 +27,9 @@ module QA ...@@ -27,7 +27,9 @@ module QA
module Resource module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox' autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group' autoload :Group, 'qa/factory/resource/group'
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project' autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :SecretVariable, 'qa/factory/resource/secret_variable' autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner' autoload :Runner, 'qa/factory/resource/runner'
...@@ -129,12 +131,22 @@ module QA ...@@ -129,12 +131,22 @@ module QA
autoload :SecretVariables, 'qa/page/project/settings/secret_variables' autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners' autoload :Runners, 'qa/page/project/settings/runners'
end end
module Issue
autoload :New, 'qa/page/project/issue/new'
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
end
end end
module Profile module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end end
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
end
module Admin module Admin
autoload :Settings, 'qa/page/admin/settings' autoload :Settings, 'qa/page/admin/settings'
end end
......
...@@ -16,20 +16,21 @@ module QA ...@@ -16,20 +16,21 @@ module QA
def build! def build!
return if overridden? return if overridden?
Builder.new(@signature).fabricate!.tap do |product| Builder.new(@signature, @factory).fabricate!.tap do |product|
@factory.public_send("#{@name}=", product) @factory.public_send("#{@name}=", product)
end end
end end
class Builder class Builder
def initialize(signature) def initialize(signature, caller_factory)
@factory = signature.factory @factory = signature.factory
@block = signature.block @block = signature.block
@caller_factory = caller_factory
end end
def fabricate! def fabricate!
@factory.fabricate! do |factory| @factory.fabricate! do |factory|
@block&.call(factory) @block&.call(factory, @caller_factory)
end end
end end
end end
......
require 'securerandom'
module QA
module Factory
module Resource
class Issue < Factory::Base
attr_writer :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
product :title do
Page::Project::Issue::Show.act { issue_title }
end
def fabricate!
project.visit!
Page::Project::Show.act do
go_to_new_issue
end
Page::Project::Issue::New.perform do |page|
page.add_title(@title)
page.add_description(@description)
page.create_new_issue
end
end
end
end
end
end
require 'securerandom'
module QA
module Factory
module Resource
class MergeRequest < Factory::Base
attr_accessor :title,
:description,
:source_branch,
:target_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
end
dependency Factory::Repository::Push, as: :target do |push, factory|
push.project = factory.project
push.branch_name = "master:#{factory.target_branch}"
end
dependency Factory::Repository::Push, as: :source do |push, factory|
push.project = factory.project
push.branch_name = "#{factory.target_branch}:#{factory.source_branch}"
push.file_name = "added_file.txt"
push.file_content = "File Added"
end
def initialize
@title = 'QA test - merge request'
@description = 'This is a test merge request'
@source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
@target_branch = "master"
end
def fabricate!
project.visit!
Page::Project::Show.act { new_merge_request }
Page::MergeRequest::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
page.create_merge_request
end
end
end
end
end
end
...@@ -42,14 +42,35 @@ module QA ...@@ -42,14 +42,35 @@ module QA
page.within(selector) { yield } if block_given? page.within(selector) { yield } if block_given?
end end
def click_element(name) # Returns true if successfully GETs the given URL
find_element(name).click # Useful because `page.status_code` is unsupported by our driver, and
# we don't have access to the `response` to use `have_http_status`.
def asset_exists?(url)
page.execute_script <<~JS
xhr = new XMLHttpRequest();
xhr.open('GET', '#{url}', true);
xhr.send();
JS
return false unless wait(time: 0.5, max: 60, reload: false) do
page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE')
end
page.evaluate_script('xhr.status') == 200
end end
def find_element(name) def find_element(name)
find(element_selector_css(name)) find(element_selector_css(name))
end end
def click_element(name)
find_element(name).click
end
def fill_element(name, content)
find_element(name).set(content)
end
def within_element(name) def within_element(name)
page.within(element_selector_css(name)) do page.within(element_selector_css(name)) do
yield yield
...@@ -76,6 +97,21 @@ module QA ...@@ -76,6 +97,21 @@ module QA
views.map(&:errors).flatten views.map(&:errors).flatten
end end
# Not tested and not expected to work with multiple dropzones
# instantiated on one page because there is no distinguishing
# attribute per dropzone file field.
def attach_file_to_dropzone(attachment, dropzone_form_container)
filename = File.basename(attachment)
field_style = { visibility: 'visible', height: '', width: '' }
attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
# Wait for link to be appended to dropzone text
wait(reload: false) do
find("#{dropzone_form_container} textarea").value.match(filename)
end
end
class DSL class DSL
attr_reader :views attr_reader :views
......
...@@ -3,23 +3,14 @@ module QA ...@@ -3,23 +3,14 @@ module QA
module Group module Group
class Show < Page::Base class Show < Page::Base
view 'app/views/groups/show.html.haml' do view 'app/views/groups/show.html.haml' do
element :dropdown_toggle, '.dropdown-toggle' element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
element :new_project_subgroup, '.new-project-subgroup' element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
element :new_project_option, /%li.*data:.*value: "new-project"/
element :new_project_toggle, element :new_project_button, /%input.*data:.*action: "new-project"/
/%li.+ data: { value: "new\-project"/ element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
element :new_project_button,
/%input.+ data: { action: "new\-project"/ # data-value and data-action get modified by JS for subgroup
element :new_subgroup_button, /%input.*\.js-new-group-child/
element :new_subgroup_toggle,
/%li.+ data: { value: "new\-subgroup"/
# TODO: input[data-action='new-subgroup'] seems to be handled by JS?
# See app/assets/javascripts/groups/new_group_child.js
end
view 'app/views/shared/groups/_search_form.html.haml' do
element :filter_by_name,
"placeholder: s_('GroupsTree|Filter by name...')"
end end
def go_to_subgroup(name) def go_to_subgroup(name)
......
...@@ -7,6 +7,8 @@ module QA ...@@ -7,6 +7,8 @@ module QA
element :settings_link, 'link_to edit_project_path' element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'" element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'" element :pipelines_settings_link, "title: 'CI / CD'"
element :issues_link, %r{link_to.*shortcuts-issues}
element :issues_link_text, "Issues"
element :top_level_items, '.sidebar-top-level-items' element :top_level_items, '.sidebar-top-level-items'
element :activity_link, "title: 'Activity'" element :activity_link, "title: 'Activity'"
end end
...@@ -43,6 +45,12 @@ module QA ...@@ -43,6 +45,12 @@ module QA
end end
end end
def click_issues
within_sidebar do
click_link('Issues')
end
end
private private
def hover_settings def hover_settings
......
module QA
module Page
module MergeRequest
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
element :issuable_create_button
end
view 'app/views/shared/issuable/form/_title.html.haml' do
element :issuable_form_title
end
view 'app/views/shared/form_elements/_description.html.haml' do
element :issuable_form_description
end
def create_merge_request
click_element :issuable_create_button
end
def fill_title(title)
fill_element :issuable_form_title, title
end
def fill_description(description)
fill_element :issuable_form_description, description
end
end
end
end
end
module QA
module Page
module Project
module Issue
class Index < Page::Base
view 'app/views/projects/issues/_issue.html.haml' do
element :issue_link, 'link_to issue.title'
end
def go_to_issue(title)
click_link(title)
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
element :submit_issue_button, 'form.submit "Submit'
end
view 'app/views/shared/issuable/form/_title.html.haml' do
element :issue_title_textbox, 'form.text_field :title'
end
view 'app/views/shared/form_elements/_description.html.haml' do
element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description"
end
def add_title(title)
fill_in 'issue_title', with: title
end
def add_description(description)
fill_in 'issue_description', with: description
end
def create_new_issue
click_on 'Submit issue'
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class Show < Page::Base
view 'app/views/projects/issues/show.html.haml' do
element :issue_details, '.issue-details'
element :title, '.title'
end
view 'app/views/shared/notes/_form.html.haml' do
element :new_note_form, 'new-note'
element :new_note_form, 'attr: :note'
end
view 'app/views/shared/notes/_comment_button.html.haml' do
element :comment_button, '%strong Comment'
end
def issue_title
find('.issue-details .title').text
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment:)
fill_in(with: text, name: 'note[note]')
attach_file_to_dropzone(attachment, '.new-note') if attachment
click_on 'Comment'
end
end
end
end
end
end
...@@ -9,10 +9,19 @@ module QA ...@@ -9,10 +9,19 @@ module QA
element :project_repository_location, 'text_field_tag :project_clone' element :project_repository_location, 'text_field_tag :project_clone'
end end
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
end
view 'app/views/projects/_home_panel.html.haml' do view 'app/views/projects/_home_panel.html.haml' do
element :project_name element :project_name
end end
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
def choose_repository_clone_http def choose_repository_clone_http
wait(reload: false) do wait(reload: false) do
click_element :clone_dropdown click_element :clone_dropdown
...@@ -34,10 +43,20 @@ module QA ...@@ -34,10 +43,20 @@ module QA
find('.qa-project-name').text find('.qa-project-name').text
end end
def new_merge_request
click_element :create_merge_request
end
def wait_for_push def wait_for_push
sleep 5 sleep 5
refresh refresh
end end
def go_to_new_issue
click_element :new_menu_toggle
click_link 'New issue'
end
end end
end end
end end
......
module QA
feature 'GitLab Geo attachment replication', :geo do
let(:file_to_attach) { File.absolute_path(File.join('spec', 'fixtures', 'banana_sample.gif')) }
scenario 'user uploads attachment to the primary node' do
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
project = Factory::Resource::Project.fabricate! do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
issue = Factory::Resource::Issue.fabricate! do |issue|
issue.title = 'My geo issue'
issue.project = project
end
Page::Project::Issue::Show.perform do |show|
show.comment('See attached banana for scale', attachment: file_to_attach)
end
Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do |session|
Page::Main::OAuth.act do
authorize! if needs_authorization?
end
expect(page).to have_content 'You are on a secondary (read-only) Geo node'
Page::Menu::Main.perform do |menu|
menu.go_to_projects
end
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(project.name)
end
Page::Menu::Side.act { click_issues }
Page::Project::Issue::Index.perform do |index|
index.go_to_issue(issue.title)
end
image_url = find('a[href$="banana_sample.gif"]')[:href]
Page::Project::Issue::Show.perform do |show|
# Wait for attachment replication
found = show.wait(reload: false) do
show.asset_exists?(image_url)
end
expect(found).to be_truthy
end
end
end
end
end
end
module QA module QA
feature 'GitLab Geo replication', :geo do feature 'GitLab Geo repository replication', :geo do
scenario 'users pushes code to the primary node' do scenario 'users pushes code to the primary node' do
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
......
module QA
feature 'creates a merge request', :core do
scenario 'user creates a new merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request'
merge_request.description = 'Great feature'
end
expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature')
expect(page).to have_content('Opened less than a minute ago')
end
end
end
...@@ -54,6 +54,19 @@ describe QA::Factory::Dependency do ...@@ -54,6 +54,19 @@ describe QA::Factory::Dependency do
expect(factory).to have_received(:mydep=).with(dependency) expect(factory).to have_received(:mydep=).with(dependency)
end end
context 'when receives a caller factory as block argument' do
let(:dependency) { QA::Factory::Base }
it 'calls given block with dependency factory and caller factory' do
allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any'))
subject.build!
expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory)
end
end
end end
end end
end end
#!/usr/bin/env ruby #!/usr/bin/env ruby
require ::File.expand_path('../lib/gitlab/popen', __dir__) # We don't have auto-loading here
require_relative '../lib/gitlab/popen'
require_relative '../lib/gitlab/popen/runner'
def emit_warnings(static_analysis)
static_analysis.warned_results.each do |result|
puts
puts "**** #{result.cmd.join(' ')} had the following warnings:"
puts
puts result.stderr
puts
end
end
def emit_errors(static_analysis)
static_analysis.failed_results.each do |result|
puts
puts "**** #{result.cmd.join(' ')} failed with the following error:"
puts
puts result.stdout
puts result.stderr
puts
end
end
tasks = [ tasks = [
%w[bundle exec rake config_lint], %w[bundle exec rake config_lint],
...@@ -17,18 +40,16 @@ tasks = [ ...@@ -17,18 +40,16 @@ tasks = [
%w[scripts/lint-rugged] %w[scripts/lint-rugged]
] ]
failed_tasks = tasks.reduce({}) do |failures, task| static_analysis = Gitlab::Popen::Runner.new
start = Time.now
puts
puts "$ #{task.join(' ')}"
output, status = Gitlab::Popen.popen(task) static_analysis.run(tasks) do |cmd, &run|
puts "==> Finished in #{Time.now - start} seconds"
puts puts
puts "$ #{cmd.join(' ')}"
failures[task.join(' ')] = output unless status.zero? result = run.call
failures puts "==> Finished in #{result.duration} seconds"
puts
end end
puts puts
...@@ -36,17 +57,20 @@ puts '===================================================' ...@@ -36,17 +57,20 @@ puts '==================================================='
puts puts
puts puts
if failed_tasks.empty? if static_analysis.all_success_and_clean?
puts 'All static analyses passed successfully.' puts 'All static analyses passed successfully.'
elsif static_analysis.all_success?
puts 'All static analyses passed successfully, but we have warnings:'
puts
emit_warnings(static_analysis)
exit 2
else else
puts 'Some static analyses failed:' puts 'Some static analyses failed:'
failed_tasks.each do |failed_task, output| emit_warnings(static_analysis)
puts emit_errors(static_analysis)
puts "**** #{failed_task} failed with the following error:"
puts
puts output
end
exit 1 exit 1
end end
...@@ -146,7 +146,7 @@ describe Ci::Build do ...@@ -146,7 +146,7 @@ describe Ci::Build do
pipeline: pipeline, pipeline: pipeline,
options: { options: {
artifacts: { artifacts: {
paths: [filename] paths: [filename, 'some-other-artifact.txt']
} }
} }
) )
......
...@@ -194,7 +194,7 @@ describe 'Commits' do ...@@ -194,7 +194,7 @@ describe 'Commits' do
end end
it 'includes the committed_date for each commit' do it 'includes the committed_date for each commit' do
commits = project.repository.commits(branch_name) commits = project.repository.commits(branch_name, limit: 40)
commits.each do |commit| commits.each do |commit|
expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}") expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}")
......
...@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do ...@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do
sign_in(user) sign_in(user)
end end
def edit_and_commit def edit_and_commit(commit_changes: true)
wait_for_requests wait_for_requests
find('.js-edit-blob').click find('.js-edit-blob').click
find('#editor') find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
click_button 'Commit changes'
if commit_changes
click_button 'Commit changes'
end
end end
context 'from MR diff' do context 'from MR diff' do
...@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do ...@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do
context 'from blob file path' do context 'from blob file path' do
before do before do
visit project_blob_path(project, tree_join(branch, file_path)) visit project_blob_path(project, tree_join(branch, file_path))
edit_and_commit
end end
it 'updates content' do it 'updates content' do
edit_and_commit
expect(page).to have_content 'successfully committed' expect(page).to have_content 'successfully committed'
expect(page).to have_content 'NextFeature' expect(page).to have_content 'NextFeature'
end end
it 'previews content' do
edit_and_commit(commit_changes: false)
click_link 'Preview changes'
wait_for_requests
old_line_count = page.all('.line_holder.old').size
new_line_count = page.all('.line_holder.new').size
expect(old_line_count).to be > 0
expect(new_line_count).to be > 0
end
end end
end end
......
...@@ -100,7 +100,7 @@ describe ApplicationHelper do ...@@ -100,7 +100,7 @@ describe ApplicationHelper do
end end
it 'returns a generic avatar' do it 'returns a generic avatar' do
expect(helper.gravatar_icon(user_email)).to match('no_avatar.png') expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
end end
end end
...@@ -110,7 +110,7 @@ describe ApplicationHelper do ...@@ -110,7 +110,7 @@ describe ApplicationHelper do
end end
it 'returns a generic avatar when email is blank' do it 'returns a generic avatar when email is blank' do
expect(helper.gravatar_icon('')).to match('no_avatar.png') expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
end end
it 'returns a valid Gravatar URL' do it 'returns a valid Gravatar URL' do
......
require 'spec_helper'
require_relative '../../config/initializers/grape_route_helpers_fix'
describe 'route shadowing' do
include GrapeRouteHelpers::NamedRouteMatcher
it 'does not occur' do
path = api_v4_projects_merge_requests_path(id: 1)
expect(path).to eq('/api/v4/projects/1/merge_requests')
path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
expect(path).to eq('/api/v4/projects/1/merge_requests/3')
end
end
...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => { ...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => {
preloadFixtures('static/create_item_dropdown.html.raw'); preloadFixtures('static/create_item_dropdown.html.raw');
let $wrapperEl; let $wrapperEl;
let createItemDropdown;
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
loadFixtures('static/create_item_dropdown.html.raw'); loadFixtures('static/create_item_dropdown.html.raw');
$wrapperEl = $('.js-create-item-dropdown-fixture-root'); $wrapperEl = $('.js-create-item-dropdown-fixture-root');
// eslint-disable-next-line no-new
new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
}); });
afterEach(() => { afterEach(() => {
$wrapperEl.remove(); $wrapperEl.remove();
}); });
it('should have a dropdown item for each piece of data', () => { describe('items', () => {
// Get the data in the dropdown beforeEach(() => {
$('.js-dropdown-menu-toggle').click(); createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should have a dropdown item for each piece of data', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
const $itemEls = $wrapperEl.find('.js-dropdown-content a'); const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
});
}); });
describe('created items', () => { describe('created items', () => {
const NEW_ITEM_TEXT = 'foobarbaz'; const NEW_ITEM_TEXT = 'foobarbaz';
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
// Open the dropdown // Open the dropdown
$('.js-dropdown-menu-toggle').click(); $('.js-dropdown-menu-toggle').click();
...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => { ...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
}); });
}); });
describe('clearDropdown()', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should clear all data and filter input', () => {
const filterInput = $wrapperEl.find('.dropdown-input-field');
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
// Filter for an item
filterInput
.val('one')
.trigger('input');
const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterFilter.length).toEqual(1);
createItemDropdown.clearDropdown();
const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterClear.length).toEqual(0);
expect(filterInput.val()).toEqual('');
});
});
describe('createNewItemFromValue option', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
createNewItemFromValue: newValue => ({
title: `${newValue}-title`,
id: `${newValue}-id`,
text: `${newValue}-text`,
}),
});
});
it('all items go through createNewItemFromValue', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
createItemAndClearInput('new-item');
const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length);
expect($($itemEls[3]).text()).toEqual('new-item-text');
expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title');
});
});
}); });
...@@ -2,55 +2,7 @@ import Vue from 'vue'; ...@@ -2,55 +2,7 @@ import Vue from 'vue';
import eventHub from '~/issuable/related_issues/event_hub'; import eventHub from '~/issuable/related_issues/event_hub';
import relatedIssuesBlock from '~/issuable/related_issues/components/related_issues_block.vue'; import relatedIssuesBlock from '~/issuable/related_issues/components/related_issues_block.vue';
const issuable1 = { import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data';
id: 200,
epic_issue_id: 1,
reference: 'foo/bar#123',
displayReference: '#123',
title: 'some title',
path: '/foo/bar/issues/123',
state: 'opened',
};
const issuable2 = {
id: 201,
epic_issue_id: 2,
reference: 'foo/bar#124',
displayReference: '#124',
title: 'some other thing',
path: '/foo/bar/issues/124',
state: 'opened',
};
const issuable3 = {
id: 202,
epic_issue_id: 3,
reference: 'foo/bar#125',
displayReference: '#125',
title: 'some other other thing',
path: '/foo/bar/issues/125',
state: 'opened',
};
const issuable4 = {
id: 203,
epic_issue_id: 4,
reference: 'foo/bar#126',
displayReference: '#126',
title: 'some other other other thing',
path: '/foo/bar/issues/126',
state: 'opened',
};
const issuable5 = {
id: 204,
epic_issue_id: 5,
reference: 'foo/bar#127',
displayReference: '#127',
title: 'some other other other thing',
path: '/foo/bar/issues/127',
state: 'opened',
};
describe('RelatedIssuesBlock', () => { describe('RelatedIssuesBlock', () => {
let RelatedIssuesBlock; let RelatedIssuesBlock;
......
...@@ -3,29 +3,7 @@ import _ from 'underscore'; ...@@ -3,29 +3,7 @@ import _ from 'underscore';
import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/issuable/related_issues/services/related_issues_service'; import relatedIssuesService from '~/issuable/related_issues/services/related_issues_service';
const defaultProps = { import { defaultProps, issuable1, issuable2 } from '../mock_data';
endpoint: '/foo/bar/issues/1/related_issues',
currentNamespacePath: 'foo',
currentProjectPath: 'bar',
};
const issuable1 = {
id: 200,
reference: 'foo/bar#123',
title: 'issue1',
path: '/foo/bar/issues/123',
state: 'opened',
relation_path: '/foo/bar/issues/123/related_issues/1',
};
const issuable2 = {
id: 201,
reference: 'foo/bar#124',
title: 'issue1',
path: '/foo/bar/issues/124',
state: 'opened',
relation_path: '/foo/bar/issues/124/related_issues/1',
};
describe('RelatedIssuesRoot', () => { describe('RelatedIssuesRoot', () => {
let RelatedIssuesRoot; let RelatedIssuesRoot;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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