Commit 3abeb175 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'gitlab-qa-130-geo-project-transfer'

 Conflicts:
   qa/qa.rb
parents cd8da77a 705d2fd2
......@@ -347,69 +347,69 @@ setup-test-env:
rspec-pg geo: *rspec-metadata-pg-geo
rspec-pg 0 26: *rspec-metadata-pg
rspec-pg 1 26: *rspec-metadata-pg
rspec-pg 2 26: *rspec-metadata-pg
rspec-pg 3 26: *rspec-metadata-pg
rspec-pg 4 26: *rspec-metadata-pg
rspec-pg 5 26: *rspec-metadata-pg
rspec-pg 6 26: *rspec-metadata-pg
rspec-pg 7 26: *rspec-metadata-pg
rspec-pg 8 26: *rspec-metadata-pg
rspec-pg 9 26: *rspec-metadata-pg
rspec-pg 10 26: *rspec-metadata-pg
rspec-pg 11 26: *rspec-metadata-pg
rspec-pg 12 26: *rspec-metadata-pg
rspec-pg 13 26: *rspec-metadata-pg
rspec-pg 14 26: *rspec-metadata-pg
rspec-pg 15 26: *rspec-metadata-pg
rspec-pg 16 26: *rspec-metadata-pg
rspec-pg 17 26: *rspec-metadata-pg
rspec-pg 18 26: *rspec-metadata-pg
rspec-pg 19 26: *rspec-metadata-pg
rspec-pg 20 26: *rspec-metadata-pg
rspec-pg 21 26: *rspec-metadata-pg
rspec-pg 22 26: *rspec-metadata-pg
rspec-pg 23 26: *rspec-metadata-pg
rspec-pg 24 26: *rspec-metadata-pg
rspec-pg 25 26: *rspec-metadata-pg
rspec-mysql 0 26: *rspec-metadata-mysql
rspec-mysql 1 26: *rspec-metadata-mysql
rspec-mysql 2 26: *rspec-metadata-mysql
rspec-mysql 3 26: *rspec-metadata-mysql
rspec-mysql 4 26: *rspec-metadata-mysql
rspec-mysql 5 26: *rspec-metadata-mysql
rspec-mysql 6 26: *rspec-metadata-mysql
rspec-mysql 7 26: *rspec-metadata-mysql
rspec-mysql 8 26: *rspec-metadata-mysql
rspec-mysql 9 26: *rspec-metadata-mysql
rspec-mysql 10 26: *rspec-metadata-mysql
rspec-mysql 11 26: *rspec-metadata-mysql
rspec-mysql 12 26: *rspec-metadata-mysql
rspec-mysql 13 26: *rspec-metadata-mysql
rspec-mysql 14 26: *rspec-metadata-mysql
rspec-mysql 15 26: *rspec-metadata-mysql
rspec-mysql 16 26: *rspec-metadata-mysql
rspec-mysql 17 26: *rspec-metadata-mysql
rspec-mysql 18 26: *rspec-metadata-mysql
rspec-mysql 19 26: *rspec-metadata-mysql
rspec-mysql 20 26: *rspec-metadata-mysql
rspec-mysql 21 26: *rspec-metadata-mysql
rspec-mysql 22 26: *rspec-metadata-mysql
rspec-mysql 23 26: *rspec-metadata-mysql
rspec-mysql 24 26: *rspec-metadata-mysql
rspec-mysql 25 26: *rspec-metadata-mysql
spinach-pg 0 4: *spinach-metadata-pg
spinach-pg 1 4: *spinach-metadata-pg
spinach-pg 2 4: *spinach-metadata-pg
spinach-pg 3 4: *spinach-metadata-pg
spinach-mysql 0 4: *spinach-metadata-mysql
spinach-mysql 1 4: *spinach-metadata-mysql
spinach-mysql 2 4: *spinach-metadata-mysql
spinach-mysql 3 4: *spinach-metadata-mysql
rspec-pg 0 27: *rspec-metadata-pg
rspec-pg 1 27: *rspec-metadata-pg
rspec-pg 2 27: *rspec-metadata-pg
rspec-pg 3 27: *rspec-metadata-pg
rspec-pg 4 27: *rspec-metadata-pg
rspec-pg 5 27: *rspec-metadata-pg
rspec-pg 6 27: *rspec-metadata-pg
rspec-pg 7 27: *rspec-metadata-pg
rspec-pg 8 27: *rspec-metadata-pg
rspec-pg 9 27: *rspec-metadata-pg
rspec-pg 10 27: *rspec-metadata-pg
rspec-pg 11 27: *rspec-metadata-pg
rspec-pg 12 27: *rspec-metadata-pg
rspec-pg 13 27: *rspec-metadata-pg
rspec-pg 14 27: *rspec-metadata-pg
rspec-pg 15 27: *rspec-metadata-pg
rspec-pg 16 27: *rspec-metadata-pg
rspec-pg 17 27: *rspec-metadata-pg
rspec-pg 18 27: *rspec-metadata-pg
rspec-pg 19 27: *rspec-metadata-pg
rspec-pg 20 27: *rspec-metadata-pg
rspec-pg 21 27: *rspec-metadata-pg
rspec-pg 22 27: *rspec-metadata-pg
rspec-pg 23 27: *rspec-metadata-pg
rspec-pg 24 27: *rspec-metadata-pg
rspec-pg 25 27: *rspec-metadata-pg
rspec-pg 26 27: *rspec-metadata-pg
rspec-mysql 0 27: *rspec-metadata-mysql
rspec-mysql 1 27: *rspec-metadata-mysql
rspec-mysql 2 27: *rspec-metadata-mysql
rspec-mysql 3 27: *rspec-metadata-mysql
rspec-mysql 4 27: *rspec-metadata-mysql
rspec-mysql 5 27: *rspec-metadata-mysql
rspec-mysql 6 27: *rspec-metadata-mysql
rspec-mysql 7 27: *rspec-metadata-mysql
rspec-mysql 8 27: *rspec-metadata-mysql
rspec-mysql 9 27: *rspec-metadata-mysql
rspec-mysql 10 27: *rspec-metadata-mysql
rspec-mysql 11 27: *rspec-metadata-mysql
rspec-mysql 12 27: *rspec-metadata-mysql
rspec-mysql 13 27: *rspec-metadata-mysql
rspec-mysql 14 27: *rspec-metadata-mysql
rspec-mysql 15 27: *rspec-metadata-mysql
rspec-mysql 16 27: *rspec-metadata-mysql
rspec-mysql 17 27: *rspec-metadata-mysql
rspec-mysql 18 27: *rspec-metadata-mysql
rspec-mysql 19 27: *rspec-metadata-mysql
rspec-mysql 20 27: *rspec-metadata-mysql
rspec-mysql 21 27: *rspec-metadata-mysql
rspec-mysql 22 27: *rspec-metadata-mysql
rspec-mysql 23 27: *rspec-metadata-mysql
rspec-mysql 24 27: *rspec-metadata-mysql
rspec-mysql 25 27: *rspec-metadata-mysql
rspec-mysql 26 27: *rspec-metadata-mysql
spinach-pg 0 3: *spinach-metadata-pg
spinach-pg 1 3: *spinach-metadata-pg
spinach-pg 2 3: *spinach-metadata-pg
spinach-mysql 0 3: *spinach-metadata-mysql
spinach-mysql 1 3: *spinach-metadata-mysql
spinach-mysql 2 3: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
......
Please view this file on the master branch, on stable branches it's out of date.
## 10.4.1 (2018-01-24)
### Fixed (1 change)
- Fix failed LDAP logins when sync_ssh_keys is included in config.
## 10.4.0 (2018-01-22)
### Security (2 changes)
......
......@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.4.1 (2018-01-24)
### Fixed (4 changes)
- Ensure that users can reclaim a namespace or project path that is blocked by an orphaned route. !16242
- Correctly escape UTF-8 path elements for uploads. !16560
- Fix issues when rendering groups and their children. !16584
- Fix bug in which projects with forks could not change visibility settings from Private to Public. !16595
### Performance (2 changes)
- rework indexes on redirect_routes.
- Remove unecessary query from labels filter.
## 10.4.0 (2018-01-22)
### Security (8 changes, 1 of them is from the community)
......
......@@ -299,6 +299,13 @@ const gfmRules = {
export class CopyAsGFM {
constructor() {
// iOS currently does not support clipboardData.setData(). This bug should
// be fixed in iOS 12, but for now we'll disable this for all iOS browsers
// ref: https://trac.webkit.org/changeset/222228/webkit
const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
if (isIOS) return;
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
$(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
......
......@@ -87,6 +87,7 @@
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
:keys="keys.enabled_keys"
:store="store"
:endpoint="endpoint"
......
......@@ -597,6 +597,10 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
.then(callDefault)
.catch(fail);
break;
case 'dashboard:groups:index':
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
case 'admin:licenses:new':
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
......
......@@ -10,7 +10,7 @@ import groupItemComponent from './components/group_item.vue';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const el = document.getElementById('js-groups-tree');
// Don't do anything if element doesn't exist (No groups)
......@@ -71,4 +71,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
});
};
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import '../../vue_shared/vue_resource_interceptor';
export default class GroupsService {
constructor(endpoint) {
......
......@@ -103,33 +103,18 @@ export default {
toggleAddRelatedIssuesForm() {
eventHub.$emit('toggleAddRelatedIssuesForm');
},
getBeforeAfterId(newIndex, lastIndex) {
let beforeId = null;
let afterId = null;
if (newIndex === 0) {
// newIndex is 0, item was moved to top => send only afterId
afterId = this.relatedIssues[newIndex].epic_issue_id;
} else if (newIndex === lastIndex) {
// newIndex is lastIndex, item was moved to bottom => send only beforeId
beforeId = this.relatedIssues[newIndex].epic_issue_id;
} else {
// leave default
beforeId = this.relatedIssues[newIndex - 1].epic_issue_id;
afterId = this.relatedIssues[newIndex].epic_issue_id;
}
getBeforeAfterId(itemEl) {
const prevItemEl = itemEl.previousElementSibling;
const nextItemEl = itemEl.nextElementSibling;
return {
beforeId,
afterId,
beforeId: prevItemEl && parseInt(prevItemEl.dataset.epicIssueId, 0),
afterId: nextItemEl && parseInt(nextItemEl.dataset.epicIssueId, 0),
};
},
reordered(event) {
this.removeDraggingCursor();
const {
beforeId,
afterId,
} = this.getBeforeAfterId(event.newIndex, this.relatedIssues.length - 1);
const { beforeId, afterId } = this.getBeforeAfterId(event.item);
this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10),
......@@ -240,6 +225,7 @@ issue-count-badge-add-button btn btn-sm btn-default"
card: canReorder
}"
:data-key="issue.id"
:data-epic-issue-id="issue.epic_issue_id"
>
<issue-item
event-namespace="relatedIssue"
......
import initGroupsList from '../../../../groups';
export default () => {
initGroupsList();
};
import GroupsList from '~/groups_list';
import Landing from '~/landing';
import initGroupsList from '../../../groups';
export default function () {
new GroupsList(); // eslint-disable-line no-new
initGroupsList();
const landingElement = document.querySelector('.js-explore-groups-landing');
if (!landingElement) return;
const exploreGroupsLanding = new Landing(
......
......@@ -5,6 +5,7 @@ import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initGroupsList from '../../../groups';
export default () => {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
......@@ -16,4 +17,6 @@ export default () => {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
initGroupsList();
};
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetChecking',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Checking ability to merge automatically
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetChecking',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="loading"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__("mrWidget|Checking ability to merge automatically") }}
</span>
</div>
</div>
</template>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetClosed',
props: {
mr: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" />
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
:author="mr.metrics.closedBy"
:dateTitle="mr.metrics.closedAt"
:dateReadable="mr.metrics.readableClosedAt"
/>
<section class="mr-info-list">
<p>
The changes were not merged into
<a
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}</a>
</p>
</section>
</div>
</div>
`,
};
<script>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetClosed',
components: {
mrWidgetAuthorTime,
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
only needs metrics and targetBranch */
mr: {
type: Object,
required: true,
default: () => ({}),
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
/>
<div class="media-body">
<mr-widget-author-time
:action-text="s__('mrWidget|Closed by')"
:author="mr.metrics.closedBy"
:date-title="mr.metrics.closedAt"
:date-readable="mr.metrics.readableClosedAt"
/>
<section class="mr-info-list">
<p>
{{ s__("mrWidget|The changes were not merged into") }}
<a
:href="mr.targetBranchPath"
class="label-branch"
>
{{ mr.targetBranch }}
</a>
</p>
</section>
</div>
</div>
</template>
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetConflicts',
props: {
mr: { type: Object, required: true },
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true" />
<div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
class="bold">
Fast-forward merge is not possible.
To merge this request, first rebase locally.
</span>
<template v-else>
<span class="bold">
There are merge conflicts<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge">
Resolve these conflicts or ask someone with write access to this repository to merge it locally
</span>
</span>
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="js-resolve-conflicts-button btn btn-default btn-xs">
Resolve conflicts
</a>
<a
v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-xs"
data-toggle="modal"
href="#modal_merge_info">
Merge locally
</a>
</template>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetConflicts',
components: {
statusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
only needs a few props */
mr: {
type: Object,
required: true,
default: () => ({}),
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
class="bold"
>
{{ s__(`mrWidget|Fast-forward merge is not possible.
To merge this request, first rebase locally.`) }}
</span>
<template v-else>
<span class="bold">
{{ s__("mrWidget|There are merge conflicts") }}<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge">
{{ s__(`mrWidget|Resolve these conflicts or ask someone
with write access to this repository to merge it locally`) }}
</span>
</span>
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="js-resolve-conflicts-button btn btn-default btn-xs"
>
{{ s__("mrWidget|Resolve conflicts") }}
</a>
<button
v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-xs"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ s__("mrWidget|Merge locally") }}
</button>
</template>
</div>
</div>
</template>
......@@ -18,11 +18,11 @@ 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 FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
......@@ -34,7 +34,7 @@ export { default as PipelineFailedState } from './components/states/mr_widget_pi
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
export { default as CheckingState } from './components/states/mr_widget_checking';
export { default as CheckingState } from './components/states/mr_widget_checking.vue';
export { default as MRWidgetStore } from 'ee/vue_merge_request_widget/stores/mr_widget_store';
export { default as MRWidgetService } from 'ee/vue_merge_request_widget/services/mr_widget_service';
export { default as eventHub } from './event_hub';
......
......@@ -15,6 +15,7 @@
# label_name: string
# sort: string
# my_reaction_emoji: string
# public_only: boolean
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
......@@ -40,7 +41,15 @@ class IssuesFinder < IssuableFinder
private
def init_collection
with_confidentiality_access_check
if public_only?
Issue.public_only
else
with_confidentiality_access_check
end
end
def public_only?
params.fetch(:public_only, false)
end
def user_can_see_all_confidential_issues?
......
......@@ -180,4 +180,8 @@ module SearchHelper
# Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code))
end
def limited_count(count, limit = 1000)
count > limit ? "#{limit}+" : count
end
end
......@@ -8,7 +8,7 @@
= (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe
%li
= _("Specify the following URL during the Runner setup:")
%code= root_url(only_path: false)
%code#coordinator_address= root_url(only_path: false)
%li
= _("Use the following registration token during setup:")
%code#registration_token= registration_token
......
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
......@@ -3,9 +3,6 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'groups'
- if params[:filter].blank? && @groups.empty?
= render 'shared/groups/empty_state'
- else
......
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
......@@ -2,9 +2,6 @@
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'groups'
- if current_user
= render 'dashboard/groups_head'
- else
......
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'groups'
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
......@@ -63,35 +63,35 @@
= link_to search_filter_path(scope: 'projects') do
Projects
%span.badge
= @search_results.projects_count
= limited_count(@search_results.limited_projects_count)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
= limited_count(@search_results.limited_issues_count)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
= limited_count(@search_results.limited_merge_requests_count)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
= @search_results.milestones_count
= limited_count(@search_results.limited_milestones_count)
- if current_application_settings.elasticsearch_search?
%li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
%span.badge
= @search_results.blobs_count
= limited_count(@search_results.blobs_count)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
%span.badge
= @search_results.commits_count
= limited_count(@search_results.commits_count)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
%span.badge
= @search_results.wiki_blobs_count
= limited_count(@search_results.wiki_blobs_count)
......@@ -3,7 +3,8 @@
= render 'shared/promotions/promote_advanced_search'
- else
.row-content-block
= search_entries_info(@search_objects, @scope, @search_term)
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
......@@ -23,4 +24,4 @@
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects'
= paginate(@search_objects, theme: 'gitlab')
= paginate_collection(@search_objects)
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('group')
- parent = @group.parent
- group_path = root_url
- group_path << parent.full_path + '/' if parent
......
---
title: Update Geo documentation to reuse the primary node SSH host key on secondary
node
merge_request: 4198
author:
type: fixed
---
title: Add details on how to disable GitLab to the DR documentation
merge_request: 4239
author:
type: changed
---
title: Fix Epic issue item reordering to handle different scenarios
merge_request: 4142
author:
type: fixed
---
title: Fix failed LDAP logins when sync_ssh_keys is included in config
title: Geo - Fix OPENSSH_EXPECTED_COMMAND in the geo:check rake task
merge_request:
author:
type: fixed
---
title: Execute group hooks after-commit when moving an issue
merge_request:
author:
type: fixed
---
title: Fix copy/paste on iOS devices due to a bug in webkit
merge_request: 15804
author:
type: fixed
---
title: rework indexes on redirect_routes
title: Optimize search queries on the search page by setting a limit for matching records.
merge_request:
author:
type: performance
---
title: Fix bug in which projects with forks could not change visibility settings from
Private to Public
merge_request: 16595
author:
type: fixed
---
title: Correctly escape UTF-8 path elements for uploads
merge_request: 16560
author:
type: fixed
---
title: Fix issues when rendering groups and their children
merge_request: 16584
author:
type: fixed
---
title: Refactors mr widget components into vue files and adds i18n
merge_request:
author:
type: other
---
title: Remove unecessary query from labels filter
merge_request:
author:
type: performance
---
title: Ensure that users can reclaim a namespace or project path that is blocked by
an orphaned route
merge_request: 16242
author:
type: fixed
......@@ -49,9 +49,6 @@ var config = {
graphs: './graphs/graphs_bundle.js',
graphs_charts: './graphs/graphs_charts.js',
graphs_show: './graphs/graphs_show.js',
group: './group.js',
groups: './groups/index.js',
groups_list: './groups_list.js',
help: './help/help.js',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
......@@ -133,9 +130,9 @@ var config = {
{
test: /\_worker\.js$/,
use: [
{
{
loader: 'worker-loader',
options: {
options: {
inline: true
}
},
......
class AddIndexUpdatedAtToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :issues, :updated_at
end
def down
remove_concurrent_index :issues, :updated_at
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180113220114) do
ActiveRecord::Schema.define(version: 20180115201419) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1224,6 +1224,7 @@ ActiveRecord::Schema.define(version: 20180113220114) do
add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "issues", ["updated_at"], name: "index_issues_on_updated_at", using: :btree
add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "keys", force: :cascade do |t|
......
......@@ -8,7 +8,7 @@
- You should make sure that all nodes run the same GitLab version.
- GitLab Geo requires PostgreSQL 9.6 and Git 2.9 in addition to GitLab's usual
[minimum requirements](../install/requirements.md)
- Using GitLab Geo in combination with High Availability is considered **Beta**
- Using GitLab Geo in combination with High Availability is considered **GA** in GitLab Enterprise Edition 10.4
>**Note:**
GitLab Geo changes significantly from release to release. Upgrades **are**
......@@ -51,6 +51,7 @@ to reading any data available in the GitLab web interface (see [current limitati
improving speed for distributed teams
- Helps reducing the loading time for automated tasks,
custom integrations and internal workflows
- A Geo secondary can be promoted to become the primary in a [Disaster Recovery](disaster-recovery.md) scenario
## Architecture
......
......@@ -85,7 +85,62 @@ Make sure the secondary instance is
running and accessible. You can login to the secondary node
with the same credentials as used in the primary.
### Step 2. (Optional) Enabling hashed storage (from GitLab 10.0)
### Step 2. Manually replicate primary SSH host keys
GitLab integrates with the system-installed SSH daemon, designating a user
(typically named git) through which all access requests are handled.
In a [Disaster Recovery](disaster-recovery.md) situation, GitLab system
administrators will promote a secondary Geo replica to a primary and they can
update the DNS records for the primary domain to point to the secondary to prevent
the need to update all references to the primary domain to the secondary domain,
like changing Git remotes and API URLs.
This will cause all SSH requests to the newly promoted primary node from
failing due to SSH host key mismatch. To prevent this, the primary SSH host
keys must be manually replicated to the secondary node.
1. SSH into the **secondary** node and login as the `root` user:
```
sudo -i
```
1. Make a backup of any existing SSH host keys:
```bash
find /etc/ssh -iname ssh_host_* -exec mv {} {}.backup.`date +%F` \;
```
1. SSH into the **primary** node, and execute the command below:
```bash
sudo find /etc/ssh -iname ssh_host_* -not -iname '*.pub'
```
1. For each file in that list copy the file from the primary node to
the **same** location on your **secondary** node.
1. On your **secondary** node, ensure the file permissions are correct:
```bash
chown root:root /etc/ssh/ssh_host_*
chmod 0600 /etc/ssh/ssh_host_*
```
1. Regenerate the public keys from the private keys:
```bash
find /etc/ssh -iname ssh_host_* -not -iname '*.backup*' -exec sh -c 'ssh-keygen -y -f "{}" > "{}.pub"' \;
```
1. Restart sshd:
```bash
service ssh restart
```
### Step 3. (Optional) Enabling hashed storage (from GitLab 10.0)
>**Warning**
Hashed storage is in **Alpha**. It is considered experimental and not
......@@ -102,7 +157,7 @@ renames no longer require synchronization between nodes.
![](img/hashed-storage.png)
### Step 3. (Optional) Configuring the secondary to trust the primary
### Step 4. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
......@@ -112,14 +167,14 @@ certificate from the primary and follow
[these instructions](https://docs.gitlab.com/omnibus/settings/ssl.html)
on the secondary.
### Step 4. Enable Git access over HTTP/HTTPS
### Step 5. Enable Git access over HTTP/HTTPS
GitLab Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Step 5. Verify proper functioning of the secondary node
### Step 6. Verify proper functioning of the secondary node
Congratulations! Your secondary geo node is now configured!
......
......@@ -77,13 +77,6 @@ be manually replicated to the secondary.
service gitlab restart
```
The secondary will start automatically replicating missing data from the
primary in a process known as backfill. Meanwhile, the primary node will start
to notify changes to the secondary, which will act on those notifications
immediately. Make sure the secondary instance is running and accessible.
### Step 2. (Optional) Enabling hashed storage
Once restarted, the secondary will automatically start replicating missing data
from the primary in a process known as backfill. Meanwhile, the primary node
will start to notify the secondary of any changes, so that the secondary can
......@@ -92,11 +85,15 @@ act on those notifications immediately.
Make sure the secondary instance is running and accessible. You can login to
the secondary node with the same credentials as used in the primary.
### Step 2. (Optional) Enabling hashed storage (from GitLab 10.0)
### Step 2. Manually replicate primary SSH host keys
Read [Manually replicate primary SSH host keys](configuration.md#step-2-manually-replicate-primary-ssh-host-keys)
### Step 3. (Optional) Enabling hashed storage (from GitLab 10.0)
Read [Enabling Hashed Storage](configuration.md#step-2-optional-enabling-hashed-storage-from-gitlab-10-0)
Read [Enabling Hashed Storage](configuration.md#step-3-optional-enabling-hashed-storage-from-gitlab-10-0)
### Step 3. (Optional) Configuring the secondary to trust the primary
### Step 4. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
......@@ -112,16 +109,16 @@ cp primary.geo.example.com.crt /usr/local/share/ca-certificates
update-ca-certificates
```
### Step 4. Enable Git access over HTTP/HTTPS
### Step 5. Enable Git access over HTTP/HTTPS
GitLab Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Step 5. Verify proper functioning of the secondary node
### Step 6. Verify proper functioning of the secondary node
Read [Verify proper functioning of the secondary node](configuration.md#step-5-verify-proper-functioning-of-the-secondary-node).
Read [Verify proper functioning of the secondary node](configuration.md#step-6-verify-proper-functioning-of-the-secondary-node).
## Selective replication
......
......@@ -17,28 +17,45 @@ See [current limitations](README.md#current-limitations) for more information.
We don't currently provide an automated way to promote a geo replica and do a
fail-over, but you can do it manually if you have `root` access to the machine.
This process promotes a secondary geo replica to a primary in the least steps.
It does not enable GitLab Geo on the newly promoted primary.
This process promotes a secondary Geo replica to a primary. To regain
geographical redundancy as quickly as possible, you should add a new secondary
immediately after following these instructions.
1. SSH into your **primary** and stop disable GitLab.
1. SSH into your **primary** to stop and disable GitLab.
```
```bash
sudo gitlab-ctl stop
```
Prevent GitLab from starting up again if the server unexpectedly reboots:
```bash
sudo systemctl disable gitlab-runsvdir
```
If you do not have SSH access to your primary take the machine offline.
Depending on the nature of your primary this may mean physically
disconnecting the machine, stopping a virtual server, reconfiguring load
balancers, or changing DNS records (see next step).
On some operating systems such as CentOS 6, an easy way to prevent GitLab
from being started if the machine reboots isn't available
(see [Omnibus issue #3058](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3058)).
It may be safest to uninstall the GitLab package completely:
Preventing the original primary from coming online during this process is
necessary to ensure data isn't added to the original primary that will not
be replicated to the newly promoted primary.
```bash
yum remove gitlab-ee
```
Preventing the original primary from coming back online during this process
is necessary prevent data from being mistakenly added to it. Any data added
after the failover process has begun will **not** be be replicated to the
newly promoted primary.
If you do not have SSH access to your primary, take the machine offline and
prevent it from rebooting by any means at your disposal. Depending on the
nature of your primary, this may mean physically disconnecting the machine,
stopping a virtual server, reconfiguring load balancers, or changing DNS
records (see next step).
1. SSH in to your **secondary** and login as root:
```
```bash
sudo -i
```
......@@ -46,7 +63,7 @@ It does not enable GitLab Geo on the newly promoted primary.
Remove the following line:
```
```ruby
## REMOVE THIS LINE
geo_secondary_role['enable'] = true
```
......@@ -57,7 +74,7 @@ It does not enable GitLab Geo on the newly promoted primary.
1. Promote the secondary to primary. Execute:
```
```bash
gitlab-ctl promote-to-primary-node
```
......@@ -73,7 +90,7 @@ secondary domain, like changing Git remotes and API URLs.
1. SSH in to your **secondary** and login as root:
```
```bash
sudo -i
```
......@@ -82,20 +99,20 @@ secondary domain, like changing Git remotes and API URLs.
After updating the primary domain's DNS records to point to the secondary,
edit `/etc/gitlab/gitlab.rb` on the the secondary to reflect the new URL:
```
```ruby
# Change the existing external_url configuration
external_url 'https://gitlab.example.com'
```
1. Reconfigure the secondary node for the change to take effect:
```
```bash
gitlab-ctl reconfigure
```
1. Execute the command below to update the newly promoted primary node URL:
```
```bash
gitlab-rake geo:update_primary_node_url
```
......
......@@ -32,6 +32,11 @@ sudo gitlab-ctl reconfigure
If you do not perform this step, you may find that two-factor authentication
[is broken following DR](faq.md#i-followed-the-disaster-recovery-instructions-and-now-two-factor-auth-is-broken).
To prevent SSH requests to the newly promoted primary node from failing
due to SSH host key mismatch when updating the primary domain's DNS record
you should perform the step to [Manually replicate primary SSH host keys](configuration.md#step-2-manually-replicate-primary-ssh-host-keys) in each
secondary node.
## Upgrading to GitLab 10.4
There are no Geo-specific steps to take!
......@@ -44,7 +49,7 @@ In GitLab 10.2, synchronizing secondaries over SSH was deprecated. In 10.3,
support is removed entirely. All installations will switch to the HTTP/HTTPS
cloning method instead. Before upgrading, ensure that all your Geo nodes are
configured to use this method and that it works for your installation. In
particular, ensure that [Git access over HTTP/HTTPS is enabled](configuration.md#step-4-enable-git-access-over-http-https).
particular, ensure that [Git access over HTTP/HTTPS is enabled](configuration.md#step-5-enable-git-access-over-http-https).
Synchronizing repositories over the public Internet using HTTP is insecure, so
you should ensure that you have HTTPS configured before upgrading. Note that
......@@ -52,7 +57,7 @@ file synchronization is **also** insecure in these cases!
## Upgrading to GitLab 10.2
### Secure PostgreSQL replication
### Secure PostgreSQL replication
Support for TLS-secured PostgreSQL replication has been added. If you are
currently using PostgreSQL replication across the open internet without an
......
......@@ -268,8 +268,10 @@ module EE
super
if group && feature_available?(:group_webhooks)
group.hooks.__send__(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
hook.async_execute(data, hooks_scope.to_s)
run_after_commit_or_now do
group.hooks.hooks_for(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
end
end
end
end
......
......@@ -22,7 +22,7 @@ module SystemCheck
\s* # optional any amount of space character
(?:\#.*)?$ # optional start-comment symbol followed by optionally any character until end of line
}x
OPENSSH_EXPECTED_COMMAND = '/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check %u %k'.freeze
OPENSSH_EXPECTED_COMMAND = '/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k'.freeze
def multi_check
unless openssh_config_exists?
......
require 'database_cleaner'
DatabaseCleaner[:active_record].strategy = :truncation, { except: ['licenses'] }
DatabaseCleaner[:active_record].strategy = :deletion, { except: ['licenses'] }
Spinach.hooks.before_scenario do
DatabaseCleaner.start
......
......@@ -179,7 +179,7 @@ module API
end
get "/search/:query", requirements: { query: /[^\/]+/ } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page])
projects = search_service.objects('projects', params[:page], false)
projects = projects.reorder(params[:order_by] => params[:sort])
present paginate(projects), with: ::API::V3::Entities::Project
......
......@@ -38,6 +38,7 @@ module Gitlab
def projects_count
@projects_count ||= projects.total_count
end
alias_method :limited_projects_count, :projects_count
def blobs_count
@blobs_count ||= blobs.total_count
......@@ -54,14 +55,17 @@ module Gitlab
def issues_count
@issues_count ||= issues.total_count
end
alias_method :limited_issues_count, :issues_count
def merge_requests_count
@merge_requests_count ||= merge_requests.total_count
end
alias_method :limited_merge_requests_count, :merge_requests_count
def milestones_count
@milestones_count ||= milestones.total_count
end
alias_method :limited_milestones_count, :milestones_count
def single_commit_result?
false
......
......@@ -20,7 +20,7 @@ module Gitlab
when 'commits'
Kaminari.paginate_array(commits).page(page).per(per_page)
else
super
super(scope, page, false)
end
end
......
......@@ -40,19 +40,21 @@ module Gitlab
@default_project_filter = default_project_filter
end
def objects(scope, page = nil)
case scope
when 'projects'
projects.page(page).per(per_page)
when 'issues'
issues.page(page).per(per_page)
when 'merge_requests'
merge_requests.page(page).per(per_page)
when 'milestones'
milestones.page(page).per(per_page)
else
Kaminari.paginate_array([]).page(page).per(per_page)
end
def objects(scope, page = nil, without_count = true)
collection = case scope
when 'projects'
projects.page(page).per(per_page)
when 'issues'
issues.page(page).per(per_page)
when 'merge_requests'
merge_requests.page(page).per(per_page)
when 'milestones'
milestones.page(page).per(per_page)
else
Kaminari.paginate_array([]).page(page).per(per_page)
end
without_count ? collection.without_count : collection
end
def projects_count
......@@ -71,18 +73,46 @@ module Gitlab
@milestones_count ||= milestones.count
end
def limited_projects_count
@limited_projects_count ||= projects.limit(count_limit).count
end
def limited_issues_count
return @limited_issues_count if @limited_issues_count
# By default getting limited count (e.g. 1000+) is fast on issuable
# collections except for issues, where filtering both not confidential
# and confidential issues user has access to, is too complex.
# It's faster to try to fetch all public issues first, then only
# if necessary try to fetch all issues.
sum = issues(public_only: true).limit(count_limit).count
@limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum
end
def limited_merge_requests_count
@limited_merge_requests_count ||= merge_requests.limit(count_limit).count
end
def limited_milestones_count
@limited_milestones_count ||= milestones.limit(count_limit).count
end
def single_commit_result?
false
end
def count_limit
1001
end
private
def projects
limit_projects.search(query)
end
def issues
issues = IssuesFinder.new(current_user).execute
def issues(finder_params = {})
issues = IssuesFinder.new(current_user, finder_params).execute
unless default_project_filter
issues = issues.where(project_id: project_ids_relation)
end
......@@ -94,13 +124,13 @@ module Gitlab
issues.full_search(query)
end
issues.order('updated_at DESC')
issues.reorder('updated_at DESC')
end
def milestones
milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.order('updated_at DESC')
milestones.reorder('updated_at DESC')
end
def merge_requests
......@@ -116,7 +146,7 @@ module Gitlab
merge_requests.full_search(query)
end
merge_requests.order('updated_at DESC')
merge_requests.reorder('updated_at DESC')
end
def default_scope
......
......@@ -16,7 +16,7 @@ module Gitlab
when 'snippet_blobs'
snippet_blobs.page(page).per(per_page)
else
super
super(scope, nil, false)
end
end
......
......@@ -28,6 +28,7 @@ module QA
autoload :Group, 'qa/factory/resource/group'
autoload :Project, 'qa/factory/resource/project'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
end
......@@ -49,7 +50,7 @@ module QA
#
autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
autoload :Entrypoint, 'qa/scenario/entrypoint'
autoload :Taggable, 'qa/scenario/taggable'
autoload :Template, 'qa/scenario/template'
##
......@@ -104,13 +105,20 @@ module QA
module Project
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
end
module Settings
autoload :Common, 'qa/page/project/settings/common'
autoload :Repository, 'qa/page/project/settings/repository'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :Advanced, 'qa/page/project/settings/advanced'
autoload :Main, 'qa/page/project/settings/main'
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :Runners, 'qa/page/project/settings/runners'
end
end
......@@ -136,10 +144,13 @@ module QA
end
##
# Classes describing shell interaction with GitLab
# Classes describing services being part of GitLab and how we can interact
# with these services, like through the shell.
#
module Shell
autoload :Omnibus, 'qa/shell/omnibus'
module Service
autoload :Shellout, 'qa/service/shellout'
autoload :Omnibus, 'qa/service/omnibus'
autoload :Runner, 'qa/service/runner'
end
##
......
......@@ -67,7 +67,7 @@ module QA
def set_replication_password
puts 'Setting replication password on primary node ...'
QA::Shell::Omnibus.new(@name).act do
QA::Service::Omnibus.new(@name).act do
gitlab_ctl 'set-replication-password', input: 'echo mypass'
end
end
......@@ -75,7 +75,7 @@ module QA
def set_primary_node
puts 'Making this node a primary node ...'
Shell::Omnibus.new(@name).act do
QA::Service::Omnibus.new(@name).act do
gitlab_ctl 'set-geo-primary-node'
end
end
......@@ -91,7 +91,7 @@ module QA
def replicate_database
puts 'Starting Geo replication on secondary node ...'
Shell::Omnibus.new(@name).act do
QA::Service::Omnibus.new(@name).act do
require 'uri'
host = URI(QA::Runtime::Scenario.geo_primary_address).host
......
......@@ -4,6 +4,12 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
product :title do
Page::Project::Settings::Repository.act do
expand_deploy_keys(&:key_title)
end
end
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test'
......@@ -13,7 +19,7 @@ module QA
project.visit!
Page::Menu::Side.act do
click_repository_setting
click_repository_settings
end
Page::Project::Settings::Repository.perform do |setting|
......
require 'securerandom'
module QA
module Factory
module Resource
class Runner < Factory::Base
attr_writer :name, :tags
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-ci-cd'
project.description = 'Project with CI/CD Pipelines'
end
def name
@name || "qa-runner-#{SecureRandom.hex(4)}"
end
def tags
@tags || %w[qa e2e]
end
def fabricate!
project.visit!
Page::Menu::Side.act { click_ci_cd_settings }
Service::Runner.new(name).tap do |runner|
Page::Project::Settings::CICD.perform do |settings|
settings.expand_runners_settings do |runners|
runner.pull
runner.token = runners.registration_token
runner.address = runners.coordinator_address
runner.tags = tags
runner.register!
end
end
end
end
end
end
end
end
......@@ -6,12 +6,29 @@ module QA
element :settings_item
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'"
element :top_level_items, '.sidebar-top-level-items'
end
def click_repository_setting
hover_setting do
click_link('Repository')
def click_repository_settings
hover_settings do
within_submenu do
click_link('Repository')
end
end
end
def click_ci_cd_settings
hover_settings do
within_submenu do
click_link('CI / CD')
end
end
end
def click_ci_cd_pipelines
within_sidebar do
click_link('CI / CD')
end
end
......@@ -23,7 +40,7 @@ module QA
private
def hover_setting
def hover_settings
within_sidebar do
find('.qa-settings-item').hover
......@@ -36,6 +53,12 @@ module QA
yield
end
end
def within_submenu
page.within('.fly-out-list') do
yield
end
end
end
end
end
......
module QA::Page
module Project::Pipeline
class Index < QA::Page::Base
view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do
element :pipeline_link, 'class="js-pipeline-url-link"'
end
def go_to_latest_pipeline
first('.js-pipeline-url-link').click
end
end
end
end
module QA::Page
module Project::Pipeline
class Show < QA::Page::Base
view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do
element :pipeline_header, /header class.*ci-header-container.*/
end
view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do
element :pipeline_graph, /class.*pipeline-graph.*/
end
view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do
element :job_component, /class.*ci-job-component.*/
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
element :status_icon, 'ci-status-icon-${status}'
end
def running?
within('.ci-header-container') do
return page.has_content?('running')
end
end
def has_build?(name, status: :success)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
return has_selector?(".ci-status-icon-#{status}")
end
end
end
end
end
end
module QA
module Page
module Project
module Settings
class CICD < Page::Base
include Common
view 'app/views/projects/settings/ci_cd/show.html.haml' do
element :runners_settings, 'Runners settings'
end
def expand_runners_settings(&block)
expand_section('Runners settings') do
Settings::Runners.perform(&block)
end
end
end
end
end
end
end
......@@ -23,6 +23,16 @@ module QA
end
end
end
def expand_section(name)
page.within('#content-body') do
page.within('section', text: name) do
click_button 'Expand'
yield
end
end
end
end
end
end
......
......@@ -10,6 +10,7 @@ module QA
view 'app/assets/javascripts/deploy_keys/components/app.vue' do
element :deploy_keys_section, /class=".*deploy\-keys.*"/
element :project_deploy_keys, 'class="qa-project-deploy-keys"'
end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
......@@ -29,9 +30,9 @@ module QA
click_on 'Add key'
end
def has_key_title?(title)
page.within('.deploy-keys') do
page.find('.title', text: title)
def key_title
page.within('.qa-project-deploy-keys') do
page.find('.title').text
end
end
end
......
module QA
module Page
module Project
module Settings
class Runners < Page::Base
view 'app/views/ci/runner/_how_to_setup_runner.html.haml' do
element :registration_token, '%code#registration_token'
element :coordinator_address, '%code#coordinator_address'
end
##
# TODO, phase-out CSS classes added in Ruby helpers.
#
view 'app/helpers/runners_helper.rb' do
# rubocop:disable Lint/InterpolationCheck
element :runner_status, 'runner-status-#{status}'
# rubocop:enable Lint/InterpolationCheck
end
def registration_token
find('code#registration_token').text
end
def coordinator_address
find('code#coordinator_address').text
end
def has_online_runner?
page.has_css?('.runner-status-online')
end
end
end
end
end
end
......@@ -33,6 +33,7 @@ module QA
def wait_for_push
sleep 5
refresh
end
end
end
......
module QA
module Scenario
##
# Base class for running the suite against any GitLab instance,
# including staging and on-premises installation.
#
class Entrypoint < Template
include Bootable
def perform(address, *files)
Runtime::Scenario.define(:gitlab_address, address)
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs|
specs.tty = true
specs.tags = self.class.get_tags
specs.files = files.any? ? files : 'qa/specs/features'
end
end
def self.tags(*tags)
@tags = tags
end
def self.get_tags
@tags
end
end
end
end
module QA
module Scenario
module Taggable
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def tags(*tags)
@tags = tags
end
def focus
@tags.to_a
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
......@@ -2,11 +2,29 @@ module QA
module Scenario
module Test
##
# Run test suite against any GitLab instance,
# Base class for running the suite against any GitLab instance,
# including staging and on-premises installation.
#
class Instance < Entrypoint
class Instance < Template
include Bootable
extend Taggable
tags :core
def perform(address, *files)
Runtime::Scenario.define(:gitlab_address, address)
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs|
specs.tty = true
specs.tags = self.class.focus
specs.files = files.any? ? files : 'qa/specs/features'
end
end
end
end
end
......
......@@ -6,7 +6,7 @@ module QA
# Run test suite against any GitLab instance where mattermost is enabled,
# including staging and on-premises installation.
#
class Mattermost < Scenario::Entrypoint
class Mattermost < Test::Instance
tags :core, :mattermost
def perform(address, mattermost, *files)
......
module QA
module Service
class Omnibus
include Scenario::Actable
include Service::Shellout
def initialize(container)
@name = container
end
def gitlab_ctl(command, input: nil)
if input.nil?
shell "docker exec #{@name} gitlab-ctl #{command}"
else
shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
end
end
end
end
end
require 'securerandom'
module QA
module Service
class Runner
include Scenario::Actable
include Service::Shellout
attr_accessor :token, :address, :tags, :image
def initialize(name)
@image = 'gitlab/gitlab-runner:alpine'
@name = name || "qa-runner-#{SecureRandom.hex(4)}"
@network = Runtime::Scenario.attributes[:network] || 'test'
@tags = %w[qa test]
end
def pull
shell "docker pull #{@image}"
end
def register!
shell <<~CMD.tr("\n", ' ')
docker run -d --rm --entrypoint=/bin/sh
--network #{@network} --name #{@name}
-e CI_SERVER_URL=#{@address}
-e REGISTER_NON_INTERACTIVE=true
-e REGISTRATION_TOKEN=#{@token}
-e RUNNER_EXECUTOR=shell
-e RUNNER_TAG_LIST=#{@tags.join(',')}
-e RUNNER_NAME=#{@name}
#{@image} -c 'gitlab-runner register && gitlab-runner run'
CMD
end
def remove!
shell "docker rm -f #{@name}"
end
end
end
end
require 'open3'
module QA
module Shell
class Omnibus
include Scenario::Actable
def initialize(container)
@name = container
end
def gitlab_ctl(command, input: nil)
if input.nil?
shell "docker exec #{@name} gitlab-ctl #{command}"
else
shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
end
end
private
module Service
module Shellout
##
# TODO, make it possible to use generic QA framework classes
# as a library - gitlab-org/gitlab-qa#94
......@@ -30,7 +14,7 @@ module QA
out.each { |line| puts line }
if wait.value.exited? && wait.value.exitstatus.nonzero?
raise "Docker command `#{command}` failed!"
raise "Command `#{command}` failed!"
end
end
end
......
......@@ -7,16 +7,12 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::DeployKey.fabricate! do |deploy_key|
deploy_key.title = deploy_key_title
deploy_key.key = deploy_key_value
deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
end
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |page|
expect(page).to have_key_title(deploy_key_title)
end
end
expect(deploy_key.title).to eq(deploy_key_title)
end
end
end
module QA
feature 'CI/CD Pipelines', :core, :docker do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
after do
Service::Runner.new(executor).remove!
end
scenario 'user registers a new specific runner' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Runner.fabricate! do |runner|
runner.name = executor
end
Page::Project::Settings::CICD.perform do |settings|
sleep 5 # Runner should register within 5 seconds
settings.refresh
settings.expand_runners_settings do |page|
expect(page).to have_content(executor)
expect(page).to have_online_runner
end
end
end
scenario 'users creates a new pipeline' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
project = Factory::Resource::Project.fabricate! do |project|
project.name = 'project-with-pipelines'
project.description = 'Project with CI/CD Pipelines.'
end
Factory::Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = %w[qa test]
end
Factory::Repository::Push.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
push.file_content = <<~EOF
test-success:
tags:
- qa
- test
script: echo 'OK'
test-failure:
tags:
- qa
- test
script:
- echo 'FAILURE'
- exit 1
test-tags:
tags:
- qa
- docker
script: echo 'NOOP'
test-artifacts:
tags:
- qa
- test
script: echo "CONTENTS" > my-artifacts/artifact.txt
artifacts:
paths:
- my-artifacts/
EOF
end
Page::Project::Show.act { wait_for_push }
expect(page).to have_content('Add .gitlab-ci.yml')
Page::Menu::Side.act { click_ci_cd_pipelines }
expect(page).to have_content('All 1')
expect(page).to have_content('Add .gitlab-ci.yml')
puts 'Waiting for the runner to process the pipeline'
sleep 15 # Runner should process all jobs within 15 seconds.
Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).to be_running
expect(pipeline).to have_build('test-success', status: :success)
expect(pipeline).to have_build('test-failure', status: :failed)
expect(pipeline).to have_build('test-tags', status: :pending)
expect(pipeline).to have_build('test-artifacts', status: :failed)
end
end
end
end
......@@ -11,10 +11,7 @@ module QA
push.commit_message = 'Add README.md'
end
Page::Project::Show.act do
wait_for_push
refresh
end
Page::Project::Show.act { wait_for_push }
expect(page).to have_content('README.md')
expect(page).to have_content('This is a test project')
......
......@@ -19,7 +19,6 @@ describe QA::Factory::Base do
it 'returns fabrication product' do
allow(subject).to receive(:new).and_return(factory)
allow(factory).to receive(:fabricate!).and_return('something')
result = subject.fabricate!('something')
......
describe QA::Scenario::Entrypoint do
describe QA::Scenario::Test::Instance do
subject do
Class.new(QA::Scenario::Entrypoint) do
Class.new(described_class) do
tags :rspec
end
end
......
......@@ -32,7 +32,7 @@ RSAAuthentication yes
PubkeyAuthentication yes
#AuthorizedKeysFile %h/.ssh/authorized_keys
#AuthorizedKeysCommand /opt/gitlab-shell/invalid_authorized_keys %u %k
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check %u %k
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommandUser git
# Don't read the user's ~/.rhosts and ~/.shosts files
......
......@@ -4,5 +4,5 @@
RSAAuthentication yes
PubkeyAuthentication yes
#AuthorizedKeysFile %h/.ssh/authorized_keys
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check %u %k # comment
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k # comment
AuthorizedKeysCommandUser anotheruser #comment with more stuff#
......@@ -5,5 +5,5 @@ RSAAuthentication yes
PubkeyAuthentication yes
#AuthorizedKeysFile %h/.ssh/authorized_keys
#AuthorizedKeysCommand /opt/gitlab-shell/invalid_authorized_keys %u %k
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check %u %k
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
#AuthorizedKeysCommandUser git
......@@ -13,7 +13,7 @@ describe Gitlab::Geo::FileTransfer do
expect(subject.file_id).to eq(upload.id)
expect(subject.filename).to eq(AvatarUploader.absolute_path(upload))
expect(Pathname.new(subject.filename).absolute?).to be_truthy
expect(subject.request_data).to eq({ id: upload.id,
expect(subject.request_data).to eq({ id: upload.model_id,
type: 'User',
checksum: upload.checksum })
end
......
......@@ -106,7 +106,7 @@ describe SystemCheck::Geo::AuthorizedKeysCheck do
it 'returns correct (uncommented) command' do
override_sshd_config('system_check/sshd_config')
expect(subject.extract_authorized_keys_command).to eq('/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check %u %k')
expect(subject.extract_authorized_keys_command).to eq('/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k')
end
it 'returns command without comments and without quotes' do
......
......@@ -22,7 +22,7 @@ feature 'Global search' do
click_button "Go"
select_filter("Issues")
expect(page).to have_selector('.gl-pagination .page', count: 2)
expect(page).to have_selector('.gl-pagination .next')
end
end
end
......@@ -34,6 +34,9 @@ describe 'New issue', :js do
click_button 'Submit issue'
# reCAPTCHA alerts when it can't contact the server, so just accept it and move on
page.driver.browser.switch_to.alert.accept
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title')
......
......@@ -108,7 +108,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
it 'shows resolved discussion when toggled' do
find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible
expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end
end
......
......@@ -10,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
PipelinesTable = Vue.extend(pipelinesTable);
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
pipeline = pipelines.find(p => p.id === 1);
PipelinesTable = Vue.extend(pipelinesTable);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
});
describe('successful request', () => {
......
......@@ -42,6 +42,16 @@ const issuable4 = {
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', () => {
let RelatedIssuesBlock;
let vm;
......@@ -148,6 +158,7 @@ describe('RelatedIssuesBlock', () => {
issuable2,
issuable3,
issuable4,
issuable5,
],
},
}).$mount();
......@@ -160,21 +171,27 @@ describe('RelatedIssuesBlock', () => {
});
it('reorder item correctly when an item is moved to the top', () => {
const beforeAfterIds = vm.getBeforeAfterId(0, 3);
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:first-child'));
expect(beforeAfterIds.beforeId).toBeNull();
expect(beforeAfterIds.afterId).toBe(1);
expect(beforeAfterIds.afterId).toBe(2);
});
it('reorder item correctly when an item is moved to the bottom', () => {
const beforeAfterIds = vm.getBeforeAfterId(3, 3);
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:last-child'));
expect(beforeAfterIds.beforeId).toBe(4);
expect(beforeAfterIds.afterId).toBeNull();
});
it('reorder item correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = vm.getBeforeAfterId(2, 3);
it('reorder item correctly when an item is swapped with adjecent item', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:nth-child(3)'));
expect(beforeAfterIds.beforeId).toBe(2);
expect(beforeAfterIds.afterId).toBe(3);
expect(beforeAfterIds.afterId).toBe(4);
});
it('reorder item correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = vm.getBeforeAfterId(vm.$el.querySelector('ul li:nth-child(4)'));
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
});
it('when expanding add related issue form', () => {
......
......@@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => {
beforeEach(() => {
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
pipeline = pipelines.find(p => p.id === 1);
pipelineWithoutAuthor = pipelines.find(p => p.id === 2);
pipelineWithoutCommit = pipelines.find(p => p.id === 3);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
pipelineWithoutAuthor = pipelines.find(p => p.user == null && p.commit !== null);
pipelineWithoutCommit = pipelines.find(p => p.user == null && p.commit == null);
});
afterEach(() => {
......
......@@ -11,9 +11,10 @@ describe('Pipelines Table', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
pipeline = pipelines.find(p => p.id === 1);
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
});
describe('table', () => {
......
import Vue from 'vue';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetChecking', () => {
describe('template', () => {
it('should have correct elements', () => {
const Component = Vue.extend(checkingComponent);
const el = new Component({
el: document.createElement('div'),
}).$el;
let Component;
let vm;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
expect(el.querySelector('button').disabled).toBeTruthy();
expect(el.innerText).toContain('Checking ability to merge automatically');
expect(el.querySelector('i')).toBeDefined();
});
beforeEach(() => {
Component = Vue.extend(checkingComponent);
vm = mountComponent(Component);
});
afterEach(() => {
vm.$destroy();
});
it('renders disabled button', () => {
expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
});
it('renders loading icon', () => {
expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
});
it('renders information about merging', () => {
expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically');
});
});
import Vue from 'vue';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
const mr = {
targetBranch: 'good-branch',
targetBranchPath: '/good-branch',
metrics: {
mergedBy: {},
mergedAt: 'mergedUpdatedAt',
closedBy: {
name: 'Fatih Acet',
username: 'fatihacet',
},
closedAt: 'closedEventUpdatedAt',
readableMergedAt: '',
readableClosedAt: '',
},
updatedAt: 'mrUpdatedAt',
closedAt: '1 day ago',
};
const createComponent = () => {
const Component = Vue.extend(closedComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetClosed', () => {
describe('props', () => {
it('should have props', () => {
const mrProp = closedComponent.props.mr;
expect(mrProp.type instanceof Object).toBeTruthy();
expect(mrProp.required).toBeTruthy();
});
let vm;
beforeEach(() => {
const Component = Vue.extend(closedComponent);
vm = mountComponent(Component, { mr: {
metrics: {
mergedBy: {},
closedBy: {
name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
readableMergedAt: '',
readableClosedAt: 'less than a minute ago',
},
targetBranchPath: '/twitter/flight/commits/so_long_jquery',
targetBranch: 'so_long_jquery',
} });
});
describe('components', () => {
it('should have components added', () => {
expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
let vm;
let el;
it('renders warning icon', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
});
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('renders closed by information with author and time', () => {
expect(
vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '),
).toContain(
'Closed by Administrator less than a minute ago',
);
});
afterEach(() => {
vm.$destroy();
});
it('links to the user that closed the MR', () => {
expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root');
});
it('should have correct elements', () => {
expect(el.querySelector('h4').textContent).toContain('Closed by');
expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name);
expect(el.textContent).toContain('The changes were not merged into');
expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
});
it('renders information about the changes not being merged', () => {
expect(
vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '),
).toContain('The changes were not merged into so_long_jquery');
});
it('should use closedEvent updatedAt as tooltip title', () => {
expect(
el.querySelector('time').getAttribute('title'),
).toBe('closedEventUpdatedAt');
});
it('renders link for target branch', () => {
expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery');
});
});
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
const ConflictsComponent = Vue.extend(conflictsComponent);
const path = '/conflicts';
describe('MRWidgetConflicts', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = conflictsComponent.props;
let Component;
let vm;
const path = '/conflicts';
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
beforeEach(() => {
Component = Vue.extend(conflictsComponent);
});
describe('template', () => {
describe('when allowed to merge', () => {
let vm;
beforeEach(() => {
vm = mountComponent(ConflictsComponent, {
mr: {
canMerge: true,
conflictResolutionPath: path,
},
});
});
afterEach(() => {
vm.$destroy();
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.$el.textContent).toContain('There are merge conflicts');
expect(vm.$el.textContent).not.toContain('ask someone with write access');
});
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button');
afterEach(() => {
vm.$destroy();
});
expect(resolveButton.textContent).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path);
describe('when allowed to merge', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mr: {
canMerge: true,
conflictResolutionPath: path,
},
});
});
it('should have merge buttons', () => {
const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
expect(mergeButton.textContent).toContain('Merge');
expect(mergeButton.disabled).toBeTruthy();
expect(mergeButton.classList.contains('btn-success')).toEqual(true);
expect(mergeLocallyButton.textContent).toContain('Merge locally');
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.$el.textContent).toContain('There are merge conflicts');
expect(vm.$el.textContent).not.toContain('ask someone with write access');
});
describe('when user does not have permission to merge', () => {
let vm;
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button');
beforeEach(() => {
vm = mountComponent(ConflictsComponent, {
mr: {
canMerge: false,
},
});
});
expect(resolveButton.textContent).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path);
});
afterEach(() => {
vm.$destroy();
});
it('should have merge buttons', () => {
const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
it('should show proper message', () => {
expect(vm.$el.textContent).toContain('ask someone with write access');
});
expect(mergeButton.textContent).toContain('Merge');
expect(mergeButton.disabled).toBeTruthy();
expect(mergeButton.classList.contains('btn-success')).toEqual(true);
expect(mergeLocallyButton.textContent).toContain('Merge locally');
});
});
it('should not have action buttons', () => {
expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
describe('when user does not have permission to merge', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mr: {
canMerge: false,
},
});
});
describe('when fast-forward or semi-linear merge enabled', () => {
let vm;
it('should show proper message', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access');
});
beforeEach(() => {
vm = mountComponent(ConflictsComponent, {
mr: {
shouldBeRebased: true,
},
});
});
it('should not have action buttons', () => {
expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
});
});
afterEach(() => {
vm.$destroy();
describe('when fast-forward or semi-linear merge enabled', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mr: {
shouldBeRebased: true,
},
});
});
it('should tell you to rebase locally', () => {
expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.');
expect(vm.$el.textContent).toContain('To merge this request, first rebase locally');
});
it('should tell you to rebase locally', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.');
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally');
});
});
});
require 'spec_helper'
describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do
describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do
let(:migration) { described_class.new }
before do
......@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
end
describe '#perform' do
it 'renames the path of system-uploads', :truncate do
it 'renames the path of system-uploads' do
upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
migration.perform('uploads/system/', 'uploads/-/system/')
......
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