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:
expire_in: 7d
paths:
- tmp/tests
- config/secrets.yml
rspec-pg geo: *rspec-metadata-pg-geo
......
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)
### Fixed (1 change)
......
......@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
### Fixed (4 changes)
......
......@@ -337,7 +337,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
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)
gem 'minitest', '~> 5.7.0'
......
......@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3)
gitlab-license (1.0.0)
gitlab-markup (1.6.3)
gitlab-styles (2.3.1)
gitlab-styles (2.3.2)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
......
/* eslint-disable class-methods-use-this */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { s__ } from './locale';
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 transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......@@ -441,13 +443,15 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
$.post(awardUrl, {
axios.post(awardUrl, {
name: emoji,
}, (data) => {
})
.then(({ data }) => {
if (data.ok) {
callback();
}
}).fail(() => new Flash('Something went wrong on our end.'));
})
.catch(() => flash(s__('Something went wrong on our end.')));
}
}
......
/* global ace */
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
......@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') {
this.$toggleButton.hide();
return $.post(currentLink.data('preview-url'), {
axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(),
}, (response) => {
currentPane.empty().append(response);
return currentPane.renderGFM();
});
})
.then(({ data }) => {
currentPane.empty().append(data);
currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
}
this.$toggleButton.show();
......
......@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
......@@ -30,15 +31,15 @@ export default class CreateItemDropdown {
filterable: true,
remote: false,
search: {
fields: ['title'],
fields: ['text'],
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel;
return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
},
fieldName: this.fieldName,
text(item) {
return _.escape(item.title);
return _.escape(item.text);
},
id(item) {
return _.escape(item.id);
......@@ -51,6 +52,11 @@ export default class CreateItemDropdown {
});
}
clearDropdown() {
this.$dropdownContainer.find('.dropdown-content').html('');
this.$dropdownContainer.find('.dropdown-input-field').val('');
}
bindEvents() {
this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
}
......@@ -58,9 +64,13 @@ export default class CreateItemDropdown {
onClickCreateWildcard(e) {
e.preventDefault();
this.refreshData();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getData(term, callback) {
......@@ -79,20 +89,28 @@ export default class CreateItemDropdown {
});
}
toggleCreateNewButton(item) {
if (item) {
this.selectedItem = {
title: item,
id: item,
text: item,
};
createNewItemFromValue(newValue) {
if (this.createNewItemFromValueOption) {
return this.createNewItemFromValueOption(newValue);
}
return {
title: newValue,
id: newValue,
text: newValue,
};
}
toggleCreateNewButton(newValue) {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer
.find('.js-dropdown-create-new-item code')
.text(item);
.text(newValue);
}
this.toggleFooter(!item);
this.toggleFooter(!newValue);
}
toggleFooter(toggleState) {
......
......@@ -115,9 +115,12 @@ export default {
reordered(event) {
this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId,
beforeId,
});
......
......@@ -176,7 +176,7 @@ export default {
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);
if (issueToReorder) {
......@@ -184,7 +184,14 @@ export default {
endpoint: issueToReorder.relation_path,
move_before_id: beforeId,
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.');
});
}
......
......@@ -16,6 +16,13 @@ class RelatedIssuesStore {
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) {
this.state.pendingReferences = issues;
}
......
......@@ -76,7 +76,13 @@
.then(data => this.store.storeDeploymentData(data))
.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'; });
},
......
......@@ -34,16 +34,23 @@
svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
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',
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data',
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',
},
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: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
......
......@@ -84,7 +84,7 @@ export default {
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
},
avatarUrl(user) {
return user.avatar || user.avatar_url;
return user.avatar || user.avatar_url || gon.default_avatar_url;
},
assigneeUrl(user) {
return `${this.rootPath}${user.username}`;
......
......@@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) {
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, 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;
......@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else {
if (avatar) {
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
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';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
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 ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue';
......
......@@ -89,7 +89,7 @@ module ApplicationHelper
end
def default_avatar
'no_avatar.png'
asset_path('no_avatar.png')
end
def last_commit(project)
......
......@@ -476,7 +476,7 @@ module Ci
if cache && project.jobs_cache_index
cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}")
key: "#{cache[:key]}_#{project.jobs_cache_index}")
end
[cache]
......
......@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username,
password: self.password,
site: URI.join(url, '/').to_s,
context_path: url.path,
context_path: url.path.chomp('/'),
auth_type: :basic,
read_timeout: 120,
use_cookies: true,
......
......@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
close_merge_requests
close_upon_missing_source_branch_ref
post_merge_manually_merged
reload_merge_requests
reset_merge_when_pipeline_succeeds
mark_pending_todos_done
......@@ -30,11 +31,22 @@ module MergeRequests
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
# 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
# target branch manually
def close_merge_requests
def post_merge_manually_merged
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 = merge_requests.select(&:diff_head_commit)
......
%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('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
......
......@@ -14,5 +14,5 @@
#{time_ago_with_tooltip(event.created_at)}
.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') }
......@@ -32,5 +32,5 @@
= icon("pencil")
- 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")
......@@ -15,7 +15,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= 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...",
supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
......
......@@ -67,7 +67,7 @@
%span.append-right-10
- 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
= form.submit 'Save changes', class: 'btn btn-save'
......
......@@ -6,7 +6,7 @@
%div{ class: div_class }
= 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?)
%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:
author:
type: fixed
---
title: Use has_table_privilege for TRIGGER on PostgreSQL
title: Return more consistent values for merge_status on MR APIs
merge_request:
author:
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:
author:
type: fixed
......@@ -8,6 +8,7 @@ require 'elasticsearch/rails/instrumentation'
module Gitlab
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/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
......
if defined?(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
# GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition.
......
......@@ -68,7 +68,7 @@ Example response:
```json
{
"file_name": "app/project.rb",
"file_path": "app/project.rb",
"branch": "master"
}
```
......@@ -98,7 +98,7 @@ Example response:
```json
{
"file_name": "app/project.rb",
"file_path": "app/project.rb",
"branch": "master"
}
```
......@@ -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'
```
Example response:
```json
{
"file_name": "app/project.rb",
"branch": "master"
}
```
Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
......
......@@ -496,7 +496,7 @@ more of the following options:
- `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
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
......
......@@ -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
the information inside the merge request.
`codeclimate.json` needs to be the only artifact file for the job. If you try
to also include other files, like Code Climate's HTML report, it will break the
Code Climate display in the merge request.
>**Note:**
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
`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
branch for your project. You can choose another branch to be your project's
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),
which means that _an issue will be closed when a merge request is merged to
the **default branch**_.
......
......@@ -63,7 +63,7 @@ module EE
private
def has_artifact?(name)
options.dig(:artifacts, :paths) == [name] &&
options.dig(:artifacts, :paths)&.include?(name) &&
artifacts_metadata?
end
end
......
......@@ -334,12 +334,6 @@ module EE
repository.async_remove_remote(::Repository::MIRROR_REMOTE)
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
bare_url = read_attribute(:import_url)
return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url)
......
......@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
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?
before_save :set_new_remote_name, if: :mirror_url_changed?
......@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base
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
update_columns(
last_error: nil,
......
......@@ -575,7 +575,15 @@ module API
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
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 :merge_commit_sha
expose :user_notes_count
......
......@@ -468,9 +468,13 @@ module Gitlab
}
options = default_options.merge(options)
options[:limit] ||= 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|
if is_enabled
gitaly_commit_client.find_commits(options)
......
......@@ -5,7 +5,17 @@ module Gitlab
module Popen
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)
raise "System commands must be given as an array of strings"
end
......@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path)
end
cmd_output = ""
cmd_status = 0
cmd_stdout = ''
cmd_stderr = ''
cmd_status = nil
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given?
stdin.close
cmd_output << stdout.read
cmd_output << stderr.read
cmd_status = wait_thr.value.exitstatus
cmd_stdout = stdout.read
cmd_stderr = stderr.read
cmd_status = wait_thr.value
end
[cmd_output, cmd_status]
Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
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:
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab
module Redis
......
require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize'
# rubocop:disable Rails/Output
module Gitlab
TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError)
......@@ -96,11 +97,9 @@ module Gitlab
end
def gid_for(group_name)
begin
Etc.getgrnam(group_name).gid
rescue ArgumentError # no group
"group #{group_name} doesn't exist"
end
Etc.getgrnam(group_name).gid
rescue ArgumentError # no group
"group #{group_name} doesn't exist"
end
def gitlab_user
......
require_relative "popen"
require_relative "version_info"
module Gitlab
class Upgrader
def execute
......
require 'tasks/gitlab/task_helpers'
module SystemCheck
module Helpers
include ::Gitlab::TaskHelpers
......
desc 'Code duplication analyze via flay'
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"
puts output
......
......@@ -4,7 +4,7 @@ namespace :gitlab do
namespace :backup do
# Create backup of 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
configure_cron_mode
......@@ -25,7 +25,7 @@ namespace :gitlab do
# Restore backup of GitLab system
desc 'GitLab | Restore a previously created backup'
task restore: :environment do
task restore: :gitlab_environment do
warn_user_is_not_gitlab
configure_cron_mode
......@@ -73,7 +73,7 @@ namespace :gitlab do
end
namespace :repo do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
......@@ -84,7 +84,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore
$progress.puts "done".color(:green)
......@@ -92,7 +92,7 @@ namespace :gitlab do
end
namespace :db do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
......@@ -103,7 +103,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore
$progress.puts "done".color(:green)
......@@ -111,7 +111,7 @@ namespace :gitlab do
end
namespace :builds do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
......@@ -122,7 +122,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore
$progress.puts "done".color(:green)
......@@ -130,7 +130,7 @@ namespace :gitlab do
end
namespace :uploads do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
......@@ -141,7 +141,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore
$progress.puts "done".color(:green)
......@@ -149,7 +149,7 @@ namespace :gitlab do
end
namespace :artifacts do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
......@@ -160,7 +160,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore
$progress.puts "done".color(:green)
......@@ -168,7 +168,7 @@ namespace :gitlab do
end
namespace :pages do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
......@@ -179,7 +179,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore
$progress.puts "done".color(:green)
......@@ -187,7 +187,7 @@ namespace :gitlab do
end
namespace :lfs do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
......@@ -198,7 +198,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore
$progress.puts "done".color(:green)
......@@ -206,7 +206,7 @@ namespace :gitlab do
end
namespace :registry do
task create: :environment do
task create: :gitlab_environment do
$progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
......@@ -221,7 +221,7 @@ namespace :gitlab do
end
end
task restore: :environment do
task restore: :gitlab_environment do
$progress.puts "Restoring container registry images ... ".color(:blue)
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
desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check
......@@ -12,7 +8,7 @@ namespace :gitlab do
namespace :app do
desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :environment do
task check: :gitlab_environment do
warn_user_is_not_gitlab
checks = [
......@@ -46,7 +42,7 @@ namespace :gitlab do
namespace :gitlab_shell do
desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do
task check: :gitlab_environment do
warn_user_is_not_gitlab
start_checking "GitLab Shell"
......@@ -254,7 +250,7 @@ namespace :gitlab do
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
task check: :gitlab_environment do
warn_user_is_not_gitlab
start_checking "Sidekiq"
......@@ -313,7 +309,7 @@ namespace :gitlab do
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
task check: :gitlab_environment do
warn_user_is_not_gitlab
if Gitlab.config.incoming_email.enabled
......@@ -336,7 +332,7 @@ namespace :gitlab do
end
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.
# This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100)
......@@ -392,7 +388,7 @@ namespace :gitlab do
namespace :repo do
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)
Rake::Task["gitlab:git:fsck"].execute
end
......@@ -400,7 +396,7 @@ namespace :gitlab do
namespace :orphans do
desc 'Gitlab | Check for orphaned namespaces and repositories'
task check: :environment do
task check: :gitlab_environment do
warn_user_is_not_gitlab
checks = [
SystemCheck::Orphans::NamespaceCheck,
......@@ -411,7 +407,7 @@ namespace :gitlab do
end
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
checks = [SystemCheck::Orphans::NamespaceCheck]
......@@ -419,7 +415,7 @@ namespace :gitlab do
end
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
checks = [SystemCheck::Orphans::RepositoryCheck]
......@@ -429,7 +425,7 @@ namespace :gitlab do
namespace :user do
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))
user = User.find_by(username: username)
if user
......
......@@ -5,7 +5,7 @@ namespace :gitlab do
HASHED_REPOSITORY_NAME = '@hashed'.freeze
desc "GitLab | Cleanup | Clean namespaces"
task dirs: :environment do
task dirs: :gitlab_environment do
warn_user_is_not_gitlab
remove_flag = ENV['REMOVE']
......@@ -79,7 +79,7 @@ namespace :gitlab do
end
desc "GitLab | Cleanup | Clean repositories"
task repos: :environment do
task repos: :gitlab_environment do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
......@@ -108,7 +108,7 @@ namespace :gitlab do
end
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
block_flag = ENV['BLOCK']
......@@ -139,7 +139,7 @@ namespace :gitlab do
# 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.
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.find_each do |project|
......
namespace :gitlab do
namespace :git do
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")
if failures.empty?
puts "Done".color(:green)
......@@ -11,7 +11,7 @@ namespace :gitlab do
end
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")
if failures.empty?
puts "Done".color(:green)
......@@ -21,7 +21,7 @@ namespace :gitlab do
end
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")
if failures.empty?
puts "Done".color(:green)
......@@ -31,7 +31,7 @@ namespace :gitlab do
end
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|
check_config_lock(repo)
check_ref_locks(repo)
......
namespace :gitlab do
namespace :gitaly do
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'
warn_user_is_not_gitlab
......
require 'tasks/gitlab/task_helpers'
# Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
namespace :gitlab do
task gitlab_environment: :environment do
extend SystemCheck::Helpers
end
namespace :gitlab do
namespace :env do
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
rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version
......
namespace :gitlab do
desc "GitLab | Setup production application"
task setup: :environment do
task setup: :gitlab_environment do
setup_db
end
......
namespace :gitlab do
namespace :shell do
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
default_version = Gitlab::Shell.version_required
......@@ -58,12 +58,12 @@ namespace :gitlab do
end
desc "GitLab | Setup gitlab-shell"
task setup: :environment do
task setup: :gitlab_environment do
setup
end
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|
path_to_repo = project.repository.path_to_repo
if File.exist?(path_to_repo)
......@@ -80,7 +80,7 @@ namespace :gitlab do
end
desc 'Create or repair repository hooks symlink'
task create_hooks: :environment do
task create_hooks: :gitlab_environment do
warn_user_is_not_gitlab
puts 'Creating/Repairing hooks symlinks for all repositories'
......
namespace :gitlab do
namespace :workhorse do
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
unless args.dir.present?
......
......@@ -2,5 +2,14 @@ unless Rails.env.production?
require 'haml_lint/rake_task'
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
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'
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
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
......
......@@ -27,7 +27,9 @@ module QA
module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group'
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
......@@ -129,12 +131,22 @@ module QA
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
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
module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
end
module Admin
autoload :Settings, 'qa/page/admin/settings'
end
......
......@@ -16,20 +16,21 @@ module QA
def build!
return if overridden?
Builder.new(@signature).fabricate!.tap do |product|
Builder.new(@signature, @factory).fabricate!.tap do |product|
@factory.public_send("#{@name}=", product)
end
end
class Builder
def initialize(signature)
def initialize(signature, caller_factory)
@factory = signature.factory
@block = signature.block
@caller_factory = caller_factory
end
def fabricate!
@factory.fabricate! do |factory|
@block&.call(factory)
@block&.call(factory, @caller_factory)
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
page.within(selector) { yield } if block_given?
end
def click_element(name)
find_element(name).click
# Returns true if successfully GETs the given URL
# 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
def find_element(name)
find(element_selector_css(name))
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)
page.within(element_selector_css(name)) do
yield
......@@ -76,6 +97,21 @@ module QA
views.map(&:errors).flatten
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
attr_reader :views
......
......@@ -3,23 +3,14 @@ module QA
module Group
class Show < Page::Base
view 'app/views/groups/show.html.haml' do
element :dropdown_toggle, '.dropdown-toggle'
element :new_project_subgroup, '.new-project-subgroup'
element :new_project_toggle,
/%li.+ data: { value: "new\-project"/
element :new_project_button,
/%input.+ data: { action: "new\-project"/
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...')"
element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
element :new_project_option, /%li.*data:.*value: "new-project"/
element :new_project_button, /%input.*data:.*action: "new-project"/
element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
# data-value and data-action get modified by JS for subgroup
element :new_subgroup_button, /%input.*\.js-new-group-child/
end
def go_to_subgroup(name)
......
......@@ -7,6 +7,8 @@ module QA
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
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 :activity_link, "title: 'Activity'"
end
......@@ -43,6 +45,12 @@ module QA
end
end
def click_issues
within_sidebar do
click_link('Issues')
end
end
private
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
element :project_repository_location, 'text_field_tag :project_clone'
end
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
end
view 'app/views/projects/_home_panel.html.haml' do
element :project_name
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
wait(reload: false) do
click_element :clone_dropdown
......@@ -34,10 +43,20 @@ module QA
find('.qa-project-name').text
end
def new_merge_request
click_element :create_merge_request
end
def wait_for_push
sleep 5
refresh
end
def go_to_new_issue
click_element :new_menu_toggle
click_link 'New issue'
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
feature 'GitLab Geo replication', :geo do
feature 'GitLab Geo repository replication', :geo do
scenario 'users pushes code to the primary node' do
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
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
expect(factory).to have_received(:mydep=).with(dependency)
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
#!/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 = [
%w[bundle exec rake config_lint],
......@@ -17,18 +40,16 @@ tasks = [
%w[scripts/lint-rugged]
]
failed_tasks = tasks.reduce({}) do |failures, task|
start = Time.now
puts
puts "$ #{task.join(' ')}"
static_analysis = Gitlab::Popen::Runner.new
output, status = Gitlab::Popen.popen(task)
puts "==> Finished in #{Time.now - start} seconds"
static_analysis.run(tasks) do |cmd, &run|
puts
puts "$ #{cmd.join(' ')}"
failures[task.join(' ')] = output unless status.zero?
result = run.call
failures
puts "==> Finished in #{result.duration} seconds"
puts
end
puts
......@@ -36,17 +57,20 @@ puts '==================================================='
puts
puts
if failed_tasks.empty?
if static_analysis.all_success_and_clean?
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
puts 'Some static analyses failed:'
failed_tasks.each do |failed_task, output|
puts
puts "**** #{failed_task} failed with the following error:"
puts
puts output
end
emit_warnings(static_analysis)
emit_errors(static_analysis)
exit 1
end
......@@ -146,7 +146,7 @@ describe Ci::Build do
pipeline: pipeline,
options: {
artifacts: {
paths: [filename]
paths: [filename, 'some-other-artifact.txt']
}
}
)
......
......@@ -194,7 +194,7 @@ describe 'Commits' do
end
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|
expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}")
......
......@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do
sign_in(user)
end
def edit_and_commit
def edit_and_commit(commit_changes: true)
wait_for_requests
find('.js-edit-blob').click
find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
click_button 'Commit changes'
if commit_changes
click_button 'Commit changes'
end
end
context 'from MR diff' do
......@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do
context 'from blob file path' do
before do
visit project_blob_path(project, tree_join(branch, file_path))
edit_and_commit
end
it 'updates content' do
edit_and_commit
expect(page).to have_content 'successfully committed'
expect(page).to have_content 'NextFeature'
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
......
......@@ -100,7 +100,7 @@ describe ApplicationHelper do
end
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
......@@ -110,7 +110,7 @@ describe ApplicationHelper do
end
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
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', () => {
preloadFixtures('static/create_item_dropdown.html.raw');
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(() => {
loadFixtures('static/create_item_dropdown.html.raw');
$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(() => {
$wrapperEl.remove();
});
it('should have a dropdown item for each piece of data', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
describe('items', () => {
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 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');
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
});
});
describe('created items', () => {
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(() => {
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
$('.js-dropdown-menu-toggle').click();
......@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
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';
import eventHub from '~/issuable/related_issues/event_hub';
import relatedIssuesBlock from '~/issuable/related_issues/components/related_issues_block.vue';
const issuable1 = {
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',
};
import { issuable1, issuable2, issuable3, issuable4, issuable5 } from '../mock_data';
describe('RelatedIssuesBlock', () => {
let RelatedIssuesBlock;
......
......@@ -3,29 +3,7 @@ import _ from 'underscore';
import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/issuable/related_issues/services/related_issues_service';
const defaultProps = {
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',
};
import { defaultProps, issuable1, issuable2 } from '../mock_data';
describe('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