summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml4
-rw-r--r--app/assets/javascripts/boards/services/board_service.js98
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/loading.vue29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js7
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/helpers/gitlab_routing_helper.rb8
-rw-r--r--app/helpers/search_helper.rb7
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/pipeline_enums.rb13
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb5
-rw-r--r--app/views/ci/variables/_variable_row.html.haml14
-rw-r--r--app/views/clusters/clusters/_form.html.haml8
-rw-r--r--app/views/shared/buttons/_project_feature_toggle.html.haml16
-rw-r--r--changelogs/unreleased/30666-fix-sr-term-style.yml5
-rw-r--r--changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml5
-rw-r--r--changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml5
-rw-r--r--changelogs/unreleased/Delete-board_service-js-in-app-folder.yml5
-rw-r--r--changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml5
-rw-r--r--changelogs/unreleased/dz-move-wiki-route.yml5
-rw-r--r--changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml5
-rw-r--r--changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml5
-rw-r--r--config/routes/project.rb9
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb69
-rw-r--r--db/schema.rb2
-rw-r--r--doc/user/project/integrations/webhooks.md4
-rw-r--r--doc/user/project/pipelines/settings.md29
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content.rb60
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb28
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/external_project.rb35
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb28
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb31
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/remote.rb27
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/repository.rb38
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/runtime.rb30
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/source.rb46
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--locale/gitlab.pot17
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb4
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb60
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb2
-rw-r--r--spec/frontend/boards/boards_store_spec.js (renamed from spec/frontend/boards/services/board_service_spec.js)75
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js3
-rw-r--r--spec/frontend/notes/components/note_app_spec.js15
-rw-r--r--spec/helpers/search_helper_spec.rb4
-rw-r--r--spec/helpers/wiki_helper_spec.rb2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js1
-rw-r--r--spec/javascripts/boards/issue_spec.js1
-rw-r--r--spec/javascripts/boards/list_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js97
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb34
-rw-r--r--spec/lib/gitlab/chat/command_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb221
-rw-r--r--spec/models/project_wiki_spec.rb4
-rw-r--r--spec/requests/api/pipelines_spec.rb2
-rw-r--r--spec/routing/project_routing_spec.rb8
-rw-r--r--spec/serializers/merge_request_poll_cached_widget_entity_spec.rb202
-rw-r--r--spec/serializers/merge_request_poll_widget_entity_spec.rb180
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb332
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb18
-rw-r--r--spec/views/search/_results.html.haml_spec.rb2
70 files changed, 1389 insertions, 687 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index c1cab844ca6..53cc0a57ee4 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -202,7 +202,7 @@
- name: redis:alpine
.use-pg10:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@@ -216,7 +216,7 @@
- name: docker.elastic.co/elasticsearch/elasticsearch:5.6.12
.use-pg10-ee:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
services:
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
deleted file mode 100644
index 03369febb4a..00000000000
--- a/app/assets/javascripts/boards/services/board_service.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/* eslint-disable class-methods-use-this */
-/**
- * This file is intended to be deleted.
- * The existing functions will removed one by one in favor of using the board store directly.
- * see https://gitlab.com/gitlab-org/gitlab-foss/issues/61621
- */
-
-import boardsStore from '~/boards/stores/boards_store';
-
-export default class BoardService {
- generateBoardsPath(id) {
- return boardsStore.generateBoardsPath(id);
- }
-
- generateIssuesPath(id) {
- return boardsStore.generateIssuesPath(id);
- }
-
- static generateIssuePath(boardId, id) {
- return boardsStore.generateIssuePath(boardId, id);
- }
-
- all() {
- return boardsStore.all();
- }
-
- generateDefaultLists() {
- return boardsStore.generateDefaultLists();
- }
-
- createList(entityId, entityType) {
- return boardsStore.createList(entityId, entityType);
- }
-
- updateList(id, position, collapsed) {
- return boardsStore.updateList(id, position, collapsed);
- }
-
- destroyList(id) {
- return boardsStore.destroyList(id);
- }
-
- getIssuesForList(id, filter = {}) {
- return boardsStore.getIssuesForList(id, filter);
- }
-
- moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
- }
-
- moveMultipleIssues({
- ids,
- fromListId = null,
- toListId = null,
- moveBeforeId = null,
- moveAfterId = null,
- }) {
- return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId });
- }
-
- newIssue(id, issue) {
- return boardsStore.newIssue(id, issue);
- }
-
- getBacklog(data) {
- return boardsStore.getBacklog(data);
- }
-
- bulkUpdate(issueIds, extraData = {}) {
- return boardsStore.bulkUpdate(issueIds, extraData);
- }
-
- static getIssueInfo(endpoint) {
- return boardsStore.getIssueInfo(endpoint);
- }
-
- static toggleIssueSubscription(endpoint) {
- return boardsStore.toggleIssueSubscription(endpoint);
- }
-
- allBoards() {
- return boardsStore.allBoards();
- }
-
- recentBoards() {
- return boardsStore.recentBoards();
- }
-
- createBoard(board) {
- return boardsStore.createBoard(board);
- }
-
- deleteBoard({ id }) {
- return boardsStore.deleteBoard({ id });
- }
-}
-
-window.BoardService = BoardService;
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 9d1de4ef8a0..7df99610132 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { mapGetters, mapActions } from 'vuex';
+import { __ } from '~/locale';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import * as constants from '../constants';
@@ -71,6 +71,9 @@ export default {
'userCanReply',
'discussionTabCounter',
]),
+ discussionTabCounterText() {
+ return this.isLoading ? '' : this.discussionTabCounter;
+ },
noteableType() {
return this.noteableData.noteableType;
},
@@ -95,9 +98,9 @@ export default {
this.fetchNotes();
}
},
- allDiscussions() {
- if (this.discussionsCount && !this.isLoading) {
- this.discussionsCount.textContent = this.discussionTabCounter;
+ discussionTabCounterText(val) {
+ if (this.discussionsCount) {
+ this.discussionsCount.textContent = val;
}
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/loading.vue b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
new file mode 100644
index 00000000000..78dc28ee92b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/loading.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <div class="prepend-top-default">
+ <div class="mr-widget-heading p-3">
+ <gl-skeleton-loader :width="577" :height="12">
+ <rect width="86" height="12" rx="2" />
+ <rect x="96" width="300" height="12" rx="2" />
+ </gl-skeleton-loader>
+ </div>
+ <div class="mr-widget-heading mr-widget-workflow p-3">
+ <gl-skeleton-loader :width="577" :height="72">
+ <rect width="120" height="12" rx="2" />
+ <rect y="20" width="300" height="12" rx="2" />
+ <rect y="40" width="60" height="12" rx="2" />
+ <rect y="40" x="68" width="100" height="12" rx="2" />
+ <rect y="60" width="40" height="12" rx="2" />
+ </gl-skeleton-loader>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 363fe226f15..31dbddbd21a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -7,6 +7,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '../flash';
+import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
@@ -44,6 +45,7 @@ export default {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'MRWidget',
components: {
+ Loading,
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer,
@@ -80,12 +82,12 @@ export default {
},
},
data() {
- const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
- const service = this.createService(store);
+ const store = this.mrData && new MRWidgetStore(this.mrData);
+
return {
mr: store,
- state: store.state,
- service,
+ state: store && store.state,
+ service: store && this.createService(store),
};
},
computed: {
@@ -133,29 +135,58 @@ export default {
}
},
},
- created() {
- this.initPolling();
- this.bindEventHubListeners();
- eventHub.$on('mr.discussion.updated', this.checkStatus);
- },
mounted() {
- this.setFaviconHelper();
- this.initDeploymentsPolling();
-
- if (this.shouldRenderMergedPipeline) {
- this.initPostMergeDeploymentsPolling();
+ if (gon && gon.features && gon.features.asyncMrWidget) {
+ MRWidgetService.fetchInitialData()
+ .then(({ data }) => this.initWidget(data))
+ .catch(() =>
+ createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
+ );
+ } else {
+ this.initWidget();
}
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
- this.pollingInterval.destroy();
- this.deploymentsInterval.destroy();
+ if (this.pollingInterval) {
+ this.pollingInterval.destroy();
+ }
+
+ if (this.deploymentsInterval) {
+ this.deploymentsInterval.destroy();
+ }
if (this.postMergeDeploymentsInterval) {
this.postMergeDeploymentsInterval.destroy();
}
},
methods: {
+ initWidget(data = {}) {
+ if (this.mr) {
+ this.mr.setData({ ...window.gl.mrWidgetData, ...data });
+ } else {
+ this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data });
+ }
+
+ if (!this.state) {
+ this.state = this.mr.state;
+ }
+
+ if (!this.service) {
+ this.service = this.createService(this.mr);
+ }
+
+ this.setFaviconHelper();
+ this.initDeploymentsPolling();
+
+ if (this.shouldRenderMergedPipeline) {
+ this.initPostMergeDeploymentsPolling();
+ }
+
+ this.initPolling();
+ this.bindEventHubListeners();
+ eventHub.$on('mr.discussion.updated', this.checkStatus);
+ },
getServiceEndpoints(store) {
return {
mergePath: store.mergePath,
@@ -319,7 +350,7 @@ export default {
};
</script>
<template>
- <div class="mr-state-widget prepend-top-default">
+ <div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
@@ -377,4 +408,5 @@ export default {
:is-post-merge="true"
/>
</div>
+ <loading v-else />
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 8a229d80954..d22cb4ced80 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -61,4 +61,11 @@ export default class MRWidgetService {
static fetchMetrics(metricsUrl) {
return axios.get(`${metricsUrl}.json`);
}
+
+ static fetchInitialData() {
+ return Promise.all([
+ axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
+ axios.get(window.gl.mrWidgetData.merge_request_widget_path),
+ ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index eb7d162e38c..c023c9e5cbd 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -51,6 +51,10 @@
position: relative;
border: 1px solid $border-color;
border-radius: $border-radius-default;
+
+ .gl-skeleton-loader {
+ display: block;
+ }
}
.mr-widget-extension {
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 844f1d04679..07f568e2a04 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -25,6 +25,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
push_frontend_feature_flag(:release_search_filter, @project, default_enabled: true)
+ push_frontend_feature_flag(:async_mr_widget, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 4d35353d5f5..e3ef8f3f2ff 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -11,7 +11,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
- push_frontend_feature_flag(:hide_dismissed_vulnerabilities)
push_frontend_feature_flag(:junit_pipeline_view)
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 38ca12e6f90..3810041b2af 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -195,7 +195,7 @@ module GitlabRoutingHelper
end
def snippet_path(snippet, *args)
- if snippet.is_a?(ProjectSnippet)
+ if snippet.type == "ProjectSnippet"
application_url_helpers.project_snippet_path(snippet.project, snippet, *args)
else
new_args = snippet_query_params(snippet, *args)
@@ -204,7 +204,7 @@ module GitlabRoutingHelper
end
def snippet_url(snippet, *args)
- if snippet.is_a?(ProjectSnippet)
+ if snippet.type == "ProjectSnippet"
application_url_helpers.project_snippet_url(snippet.project, snippet, *args)
else
new_args = snippet_query_params(snippet, *args)
@@ -213,7 +213,7 @@ module GitlabRoutingHelper
end
def raw_snippet_path(snippet, *args)
- if snippet.is_a?(ProjectSnippet)
+ if snippet.type == "ProjectSnippet"
application_url_helpers.raw_project_snippet_path(snippet.project, snippet, *args)
else
new_args = snippet_query_params(snippet, *args)
@@ -222,7 +222,7 @@ module GitlabRoutingHelper
end
def raw_snippet_url(snippet, *args)
- if snippet.is_a?(ProjectSnippet)
+ if snippet.type == "ProjectSnippet"
application_url_helpers.raw_project_snippet_url(snippet.project, snippet, *args)
else
new_args = snippet_query_params(snippet, *args)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 777fe82e4c0..a89fea4b7b8 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,13 +31,14 @@ module SearchHelper
from = collection.offset_value + 1
to = collection.offset_value + collection.to_a.size
count = collection.total_count
+ term_element = "<span>&nbsp;<code>#{h(term)}</code>&nbsp;</span>".html_safe
search_entries_info_template(collection) % {
from: from,
to: to,
count: count,
scope: search_entries_scope_label(scope, count),
- term: term
+ term_element: term_element
}
end
@@ -72,9 +73,9 @@ module SearchHelper
def search_entries_info_template(collection)
if collection.total_pages > 1
- s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"")
+ s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}").html_safe
else
- s_("SearchResults|Showing %{count} %{scope} for \"%{term}\"")
+ s_("SearchResults|Showing %{count} %{scope} for%{term_element}").html_safe
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5821cc1a1a5..c3292d7524e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -204,7 +204,7 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
- scope :ci_sources, -> { where(config_source: ci_sources_values) }
+ scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
@@ -315,10 +315,6 @@ module Ci
sources.reject { |source| source == "external" }.values
end
- def self.ci_sources_values
- config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
- end
-
def self.bridgeable_statuses
::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending]
end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 859abc4a0d5..ac930f63abf 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -35,9 +35,20 @@ module Ci
{
unknown_source: nil,
repository_source: 1,
- auto_devops_source: 2
+ auto_devops_source: 2,
+ remote_source: 4,
+ external_project_source: 5
}
end
+
+ def self.ci_config_sources_values
+ config_sources.values_at(
+ :unknown_source,
+ :repository_source,
+ :auto_devops_source,
+ :remote_source,
+ :external_project_source)
+ end
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index f02ccd9e55e..48c96203921 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -58,7 +58,7 @@ class ProjectWiki
end
def wiki_base_path
- [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('')
+ [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/-', '/wikis'].join('')
end
# Returns the Gitlab::Git::Wiki object.
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index eda7a36c2ee..2a81931c49f 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -3,6 +3,9 @@
class MergeRequestWidgetEntity < Grape::Entity
include RequestAwareEntity
+ expose :id
+ expose :iid
+
expose :source_project_full_path do |merge_request|
merge_request.source_project&.full_path
end
@@ -65,6 +68,8 @@ class MergeRequestWidgetEntity < Grape::Entity
end
def as_json(options = {})
+ return super(options) if Feature.enabled?(:async_mr_widget)
+
super(options)
.merge(MergeRequestPollCachedWidgetEntity.new(object, **@options.opts_hash).as_json(options))
.merge(MergeRequestPollWidgetEntity.new(object, **@options.opts_hash).as_json(options))
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index ed9b3ab1940..4244556a24a 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -44,31 +44,21 @@
.ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0
.append-right-default
= s_("CiVariable|Protected")
- %button{ type: 'button',
- class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_protected}",
- "aria-label": s_("CiVariable|Toggle protected") }
+ = render "shared/buttons/project_feature_toggle", is_checked: is_protected, label: s_("CiVariable|Toggle protected") do
%input{ type: "hidden",
class: 'js-ci-variable-input-protected js-project-feature-toggle-input',
name: protected_input_name,
value: is_protected,
data: { default: is_protected_default.to_s } }
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.ci-variable-body-item.ci-variable-masked-item.table-section.section-20.mr-0.border-top-0
.append-right-default
= s_("CiVariable|Masked")
- %button{ type: 'button',
- class: "js-project-feature-toggle project-feature-toggle qa-variable-masked #{'is-checked' if is_masked}",
- "aria-label": s_("CiVariable|Toggle masked") }
+ = render "shared/buttons/project_feature_toggle", is_checked: is_masked, label: s_("CiVariable|Toggle masked"), class_list: "js-project-feature-toggle project-feature-toggle qa-variable-masked" do
%input{ type: "hidden",
class: 'js-ci-variable-input-masked js-project-feature-toggle-input',
name: masked_input_name,
value: is_masked,
data: { default: is_masked_default.to_s } }
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
= render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable
%button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml
index 3d0266a2d5b..f9085b781fb 100644
--- a/app/views/clusters/clusters/_form.html.haml
+++ b/app/views/clusters/clusters/_form.html.haml
@@ -3,14 +3,8 @@
.form-group
%h5= s_('ClusterIntegration|Integration status')
%label.append-bottom-0.js-cluster-enable-toggle-area
- %button{ type: 'button',
- class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
- "aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
- disabled: !can?(current_user, :update_cluster, @cluster) }
+ = render "shared/buttons/project_feature_toggle", is_checked: @cluster.enabled?, label: s_("ClusterIntegration|Toggle Kubernetes cluster"), disabled: !can?(current_user, :update_cluster, @cluster) do
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
.form-group
diff --git a/app/views/shared/buttons/_project_feature_toggle.html.haml b/app/views/shared/buttons/_project_feature_toggle.html.haml
new file mode 100644
index 00000000000..0f630786455
--- /dev/null
+++ b/app/views/shared/buttons/_project_feature_toggle.html.haml
@@ -0,0 +1,16 @@
+- class_list ||= "js-project-feature-toggle project-feature-toggle"
+- data ||= nil
+- disabled ||= false
+- is_checked ||= false
+- label ||= nil
+
+%button{ type: 'button',
+ class: "#{class_list} #{'is-disabled' if disabled} #{'is-checked' if is_checked}",
+ "aria-label": label,
+ disabled: disabled,
+ data: data }
+ - if yield.present?
+ = yield
+ %span.toggle-icon
+ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
+ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
diff --git a/changelogs/unreleased/30666-fix-sr-term-style.yml b/changelogs/unreleased/30666-fix-sr-term-style.yml
new file mode 100644
index 00000000000..e69a7f49ae8
--- /dev/null
+++ b/changelogs/unreleased/30666-fix-sr-term-style.yml
@@ -0,0 +1,5 @@
+---
+title: Changes to how the search term is styled in the results
+merge_request: 20416
+author:
+type: changed
diff --git a/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml b/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml
new file mode 100644
index 00000000000..8949eb6881a
--- /dev/null
+++ b/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml
@@ -0,0 +1,5 @@
+---
+title: Added legend to deploy boards
+merge_request: 20208
+author:
+type: added
diff --git a/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml b/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml
new file mode 100644
index 00000000000..65ace335e1e
--- /dev/null
+++ b/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml
@@ -0,0 +1,5 @@
+---
+title: Allow NPM package downloads with CI_JOB_TOKEN
+merge_request: 20868
+author:
+type: added
diff --git a/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml b/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml
new file mode 100644
index 00000000000..52027fa603f
--- /dev/null
+++ b/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml
@@ -0,0 +1,5 @@
+---
+title: delete board_service.js
+merge_request: 20168
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml b/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml
new file mode 100644
index 00000000000..3b292bf29e5
--- /dev/null
+++ b/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml
@@ -0,0 +1,5 @@
+---
+title: Allow CI config path to point to a URL or file in a different repository
+merge_request: 20179
+author:
+type: added
diff --git a/changelogs/unreleased/dz-move-wiki-route.yml b/changelogs/unreleased/dz-move-wiki-route.yml
new file mode 100644
index 00000000000..f4fa96d8822
--- /dev/null
+++ b/changelogs/unreleased/dz-move-wiki-route.yml
@@ -0,0 +1,5 @@
+---
+title: Move wiki routing under /-/ scope
+merge_request: 21185
+author:
+type: deprecated
diff --git a/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml b/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml
new file mode 100644
index 00000000000..b3325a81882
--- /dev/null
+++ b/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml
@@ -0,0 +1,5 @@
+---
+title: Fix snippet routes
+merge_request: 21248
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml b/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml
new file mode 100644
index 00000000000..0046416978a
--- /dev/null
+++ b/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml
@@ -0,0 +1,5 @@
+---
+title: Fetches initial merge request widget data async
+merge_request: 20719
+author:
+type: changed
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 8a5e20c8eff..2cc04da6f35 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -258,6 +258,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :list_projects
end
end
+
+ # The wiki routing contains wildcard characters so
+ # its preferable to keep it below all other project routes
+ draw :wiki
end
# End of the /-/ scope.
@@ -523,9 +527,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :web_ide_clientside_preview
end
- # Since both wiki and repository routing contains wildcard characters
+ # The repository routing contains wildcard characters so
# its preferable to keep it below all other project routes
- draw :wiki
draw :repository
# All new routes should go under /-/ scope.
@@ -542,7 +545,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
:forks, :group_links, :import, :avatar, :mirror,
:cycle_analytics, :mattermost, :variables, :triggers,
:environments, :protected_environments, :error_tracking,
- :serverless, :clusters, :audit_events)
+ :serverless, :clusters, :audit_events, :wikis)
end
# rubocop: disable Cop/PutProjectRoutesUnderScope
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 3a43f515a1b..c0be2f66ca7 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -185,7 +185,7 @@ module.exports = {
options: { limit: 2048 },
},
{
- test: /\_worker\.js$/,
+ test: /_worker\.js$/,
use: [
{
loader: 'worker-loader',
diff --git a/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb b/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb
new file mode 100644
index 00000000000..580d3a189c8
--- /dev/null
+++ b/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class AddUniqueConstraintToSoftwareLicenses < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+ NEW_INDEX = 'index_software_licenses_on_unique_name'
+ OLD_INDEX = 'index_software_licenses_on_name'
+
+ disable_ddl_transaction!
+
+ # 12 software licenses will be removed on GitLab.com
+ # 0 software license policies will be updated on GitLab.com
+ def up(attempts: 100)
+ remove_redundant_software_licenses!
+
+ add_concurrent_index :software_licenses, :name, unique: true, name: NEW_INDEX
+ remove_concurrent_index :software_licenses, :name, name: OLD_INDEX
+ rescue ActiveRecord::RecordNotUnique
+ retry if (attempts -= 1) > 0
+
+ raise StandardError, <<~EOS
+ Failed to add an unique index to software_licenses, despite retrying the
+ migration 100 times.
+
+ See https://gitlab.com/gitlab-org/gitlab/merge_requests/19840.
+ EOS
+ end
+
+ def down
+ remove_concurrent_index :software_licenses, :name, unique: true, name: NEW_INDEX
+ add_concurrent_index :software_licenses, :name, name: OLD_INDEX
+ end
+
+ private
+
+ def remove_redundant_software_licenses!
+ redundant_software_licenses = execute <<~SQL
+ SELECT min(id) id, name
+ FROM software_licenses
+ WHERE name IN (select name from software_licenses group by name having count(name) > 1)
+ GROUP BY name
+ SQL
+ say "Detected #{redundant_software_licenses.count} duplicates."
+
+ redundant_software_licenses.each_row do |id, name|
+ say_with_time("Reassigning policies that reference software license #{name}.") do
+ duplicates = software_licenses.where.not(id: id).where(name: name)
+
+ software_license_policies
+ .where(software_license_id: duplicates)
+ .update_all(software_license_id: id)
+
+ duplicates.delete_all
+ end
+ end
+ end
+
+ def table(name)
+ Class.new(ActiveRecord::Base) { self.table_name = name }
+ end
+
+ def software_licenses
+ @software_licenses ||= table(:software_licenses)
+ end
+
+ def software_license_policies
+ @software_license_policies ||= table(:software_license_policies)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c6fa84e96b5..621ae7f380e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3698,7 +3698,7 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do
create_table "software_licenses", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "spdx_identifier", limit: 255
- t.index ["name"], name: "index_software_licenses_on_name"
+ t.index ["name"], name: "index_software_licenses_on_unique_name", unique: true
t.index ["spdx_identifier"], name: "index_software_licenses_on_spdx_identifier"
end
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 2f2ab65c5dc..1e229db8b2e 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -969,7 +969,7 @@ X-Gitlab-Event: Wiki Page Hook
"http_url": "http://example.com/root/awesome-project.git"
},
"wiki": {
- "web_url": "http://example.com/root/awesome-project/wikis/home",
+ "web_url": "http://example.com/root/awesome-project/-/wikis/home",
"git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
"git_http_url": "http://example.com/root/awesome-project.wiki.git",
"path_with_namespace": "root/awesome-project.wiki",
@@ -981,7 +981,7 @@ X-Gitlab-Event: Wiki Page Hook
"format": "markdown",
"message": "adding an awesome page to the wiki",
"slug": "awesome",
- "url": "http://example.com/root/awesome-project/wikis/awesome",
+ "url": "http://example.com/root/awesome-project/-/wikis/awesome",
"action": "create"
}
}
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 6480c7e0af9..a63da5c442f 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -67,20 +67,37 @@ For information about setting a maximum artifact size for a project, see
## Custom CI configuration path
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4.
+> - [Support for external `.gitlab-ci.yml` locations](https://gitlab.com/gitlab-org/gitlab/issues/14376) introduced in GitLab 12.6.
By default we look for the `.gitlab-ci.yml` file in the project's root
-directory. If you require a different location **within** the repository,
-you can set a custom path that will be used to look up the configuration file,
-this path should be **relative** to the root.
+directory. If needed, you can specify an alternate path and file name, including locations outside the project.
-Here are some valid examples:
+Hosting the configuration file in a separate project will allow stricter control of the
+configuration file. You can limit access to the project hosting the configuration to only people
+with proper authorization, and users can use the configuration for their pipelines,
+without being able to modify it.
-- `.gitlab-ci.yml`
+If the CI configuration will stay within the repository, but in a
+location different than the default,
+the path must be relative to the root directory. Examples of valid paths and file names:
+
+- `.gitlab-ci.yml` (default)
- `.my-custom-file.yml`
- `my/path/.gitlab-ci.yml`
- `my/path/.my-custom-file.yml`
+If the CI configuration will be hosted in a different project within GitLab, the path must be relative
+to the root directory in the other project, with the group and project name added to the end:
+
+- `.gitlab-ci.yml@mygroup/another-project`
+- `my/path/.my-custom-file.yml@mygroup/another-project`
+
+If the CI configuration will be hosted on an external site, different than the GitLab instance,
+the URL link must end with `.yml`:
+
+- `http://example.com/generate/ci/config.yml`
+
The path can be customized at a project level. To customize the path:
1. Go to the project's **Settings > CI / CD**.
diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb
index a8cd99b8e92..d4b7444005e 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content.rb
@@ -8,21 +8,28 @@ module Gitlab
class Content < Chain::Base
include Chain::Helpers
- def perform!
- return if @command.config_content
-
- if content = content_from_repo
- @command.config_content = content
- @pipeline.config_source = :repository_source
- # TODO: we should persist ci_config_path
- # @pipeline.config_path = ci_config_path
- elsif content = content_from_auto_devops
- @command.config_content = content
- @pipeline.config_source = :auto_devops_source
- end
+ SOURCES = [
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops
+ ].freeze
+
+ LEGACY_SOURCES = [
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
+ ].freeze
- unless @command.config_content
- return error("Missing #{ci_config_path} file")
+ def perform!
+ if config = find_config
+ # TODO: we should persist config_content
+ # @pipeline.config_content = config.content
+ @command.config_content = config.content
+ @pipeline.config_source = config.source
+ else
+ error('Missing CI config file')
end
end
@@ -32,24 +39,21 @@ module Gitlab
private
- def content_from_repo
- return unless project
- return unless @pipeline.sha
- return unless ci_config_path
+ def find_config
+ sources.each do |source|
+ config = source.new(@pipeline, @command)
+ return config if config.exists?
+ end
- project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path)
- rescue GRPC::NotFound, GRPC::Internal
nil
end
- def content_from_auto_devops
- return unless project&.auto_devops_enabled?
-
- Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
- end
-
- def ci_config_path
- project.ci_config_path.presence || '.gitlab-ci.yml'
+ def sources
+ if Feature.enabled?(:ci_root_config_content, @command.project, default_enabled: true)
+ SOURCES
+ else
+ LEGACY_SOURCES
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb
new file mode 100644
index 00000000000..e9bcc67de9c
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class AutoDevops < Source
+ def content
+ strong_memoize(:content) do
+ next unless project&.auto_devops_enabled?
+
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
+ YAML.dump('include' => [{ 'template' => template.full_name }])
+ end
+ end
+
+ def source
+ :auto_devops_source
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb b/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb
new file mode 100644
index 00000000000..8a19e433483
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class ExternalProject < Source
+ def content
+ strong_memoize(:content) do
+ next unless external_project_path?
+
+ path_file, path_project = ci_config_path.split('@', 2)
+ YAML.dump('include' => [{ 'project' => path_project, 'file' => path_file }])
+ end
+ end
+
+ def source
+ :external_project_source
+ end
+
+ private
+
+ # Example: path/to/.gitlab-ci.yml@another-group/another-project
+ def external_project_path?
+ ci_config_path =~ /\A.+(yml|yaml)@.+\z/
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb
new file mode 100644
index 00000000000..c4cef356628
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class LegacyAutoDevops < Source
+ def content
+ strong_memoize(:content) do
+ next unless project&.auto_devops_enabled?
+
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
+ template.content
+ end
+ end
+
+ def source
+ :auto_devops_source
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb
new file mode 100644
index 00000000000..fa4a97c6880
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class LegacyRepository < Source
+ def content
+ strong_memoize(:content) do
+ next unless project
+ next unless @pipeline.sha
+ next unless ci_config_path
+
+ project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path)
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+ end
+
+ def source
+ :repository_source
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb
new file mode 100644
index 00000000000..dcc336b8929
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class Remote < Source
+ def content
+ strong_memoize(:content) do
+ next unless ci_config_path =~ URI.regexp(%w[http https])
+
+ YAML.dump('include' => [{ 'remote' => ci_config_path }])
+ end
+ end
+
+ def source
+ :remote_source
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/repository.rb b/lib/gitlab/ci/pipeline/chain/config/content/repository.rb
new file mode 100644
index 00000000000..0752b099d3d
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/repository.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class Repository < Source
+ def content
+ strong_memoize(:content) do
+ next unless file_in_repository?
+
+ YAML.dump('include' => [{ 'local' => ci_config_path }])
+ end
+ end
+
+ def source
+ :repository_source
+ end
+
+ private
+
+ def file_in_repository?
+ return unless project
+ return unless @pipeline.sha
+
+ project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path).present?
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb b/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb
new file mode 100644
index 00000000000..4811d3d913d
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class Runtime < Source
+ def content
+ @command.config_content
+ end
+
+ def source
+ # The only case when this source is used is when the config content
+ # is passed in as parameter to Ci::CreatePipelineService.
+ # This would only occur with parent/child pipelines which is being
+ # implemented.
+ # TODO: change source to return :runtime_source
+ # https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
+
+ nil
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/source.rb b/lib/gitlab/ci/pipeline/chain/config/content/source.rb
new file mode 100644
index 00000000000..3389187473b
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/source.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class Source
+ include Gitlab::Utils::StrongMemoize
+
+ DEFAULT_YAML_FILE = '.gitlab-ci.yml'
+
+ def initialize(pipeline, command)
+ @pipeline = pipeline
+ @command = command
+ end
+
+ def exists?
+ strong_memoize(:exists) do
+ content.present?
+ end
+ end
+
+ def content
+ raise NotImplementedError
+ end
+
+ def source
+ raise NotImplementedError
+ end
+
+ def project
+ @project ||= @pipeline.project
+ end
+
+ def ci_config_path
+ @ci_config_path ||= project.ci_config_path.presence || DEFAULT_YAML_FILE
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c89b8f563dc..488e8e0fcea 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -105,7 +105,7 @@ module Gitlab
save_id_mapping(relation_key, data_hash, relation_object)
rescue => e
# re-raise if not enabled
- raise e unless Feature.enabled?(:import_graceful_failures, @project.group)
+ raise e unless Feature.enabled?(:import_graceful_failures, @project.group, default_enabled: true)
log_import_failure(relation_key, relation_index, e)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d556c699bd3..58b9c7d905a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2960,6 +2960,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Canary"
+msgstr ""
+
msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
msgstr ""
@@ -15325,16 +15328,16 @@ msgstr ""
msgid "SearchCodeResults|of %{link_to_project}"
msgstr ""
-msgid "SearchResults|Showing %{count} %{scope} for \"%{term}\""
+msgid "SearchResults|Showing %{count} %{scope} for%{term_element}"
msgstr ""
-msgid "SearchResults|Showing %{count} %{scope} for \"%{term}\" in your personal and project snippets"
+msgid "SearchResults|Showing %{count} %{scope} for%{term_element} in your personal and project snippets"
msgstr ""
-msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\""
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}"
msgstr ""
-msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\" in your personal and project snippets"
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element} in your personal and project snippets"
msgstr ""
msgid "SearchResults|We couldn't find any %{scope} matching %{term}"
@@ -16963,6 +16966,9 @@ msgstr ""
msgid "Subtracts"
msgstr ""
+msgid "Succeeded"
+msgstr ""
+
msgid "Successfully activated"
msgstr ""
@@ -18760,6 +18766,9 @@ msgstr ""
msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
+msgid "Unable to load the merge request widget. Try reloading the page."
+msgstr ""
+
msgid "Unable to resolve"
msgstr ""
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 4519cd014a1..ef5f286502b 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1073,7 +1073,7 @@ describe Projects::MergeRequestsController do
end
it 'renders MergeRequest as JSON' do
- expect(json_response.keys).to include('id', 'iid', 'description')
+ expect(json_response.keys).to include('id', 'iid')
end
end
@@ -1107,7 +1107,7 @@ describe Projects::MergeRequestsController do
it 'renders MergeRequest as JSON' do
subject
- expect(json_response.keys).to include('id', 'iid', 'description')
+ expect(json_response.keys).to include('id', 'iid')
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f6eeb8d7065..f5558a1f2ec 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -706,7 +706,7 @@ describe 'Pipelines', :js do
click_on 'Run Pipeline'
end
- it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ it { expect(page).to have_content('Missing CI config file') }
it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
click_button project.default_branch
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 5c6b04a7141..331ba58d067 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -29,11 +29,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -43,11 +43,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -57,11 +57,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
end
@@ -77,11 +77,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -95,11 +95,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -113,11 +113,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page).to have_content("regular link")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
- expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>")
end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 56d0518015d..499c459621a 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -55,7 +55,7 @@ describe "User creates wiki page" do
end
expect(current_path).to include("one/two/three-test")
- expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
+ expect(page).to have_xpath("//a[@href='/#{project.full_path}/-/wikis/one/two/three-test']")
end
it "has `Create home` as a commit message", :js do
diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/boards_store_spec.js
index 86f49f63f4e..f8e5873c87b 100644
--- a/spec/frontend/boards/services/board_service_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -1,10 +1,9 @@
-import BoardService from '~/boards/services/board_service';
import { TEST_HOST } from 'helpers/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
-describe('BoardService', () => {
+describe('boardsStore', () => {
const dummyResponse = "without type checking this doesn't matter";
const boardId = 'dummy-board-id';
const endpoints = {
@@ -14,7 +13,6 @@ describe('BoardService', () => {
recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
};
- let service;
let axiosMock;
beforeEach(() => {
@@ -23,7 +21,6 @@ describe('BoardService', () => {
...endpoints,
boardId,
});
- service = new BoardService();
});
describe('all', () => {
@@ -31,13 +28,13 @@ describe('BoardService', () => {
axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.all()).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.all()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
- return expect(service.all()).rejects.toThrow();
+ return expect(boardsStore.all()).rejects.toThrow();
});
});
@@ -48,13 +45,13 @@ describe('BoardService', () => {
axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.generateDefaultLists()).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.generateDefaultLists()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
- return expect(service.generateDefaultLists()).rejects.toThrow();
+ return expect(boardsStore.generateDefaultLists()).rejects.toThrow();
});
});
@@ -76,7 +73,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.createList(entityId, entityType))
+ return expect(boardsStore.createList(entityId, entityType))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -86,7 +83,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.createList(entityId, entityType))
+ return expect(boardsStore.createList(entityId, entityType))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -113,7 +110,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.updateList(id, position, collapsed))
+ return expect(boardsStore.updateList(id, position, collapsed))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -123,7 +120,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.updateList(id, position, collapsed))
+ return expect(boardsStore.updateList(id, position, collapsed))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -147,7 +144,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.destroyList(id))
+ return expect(boardsStore.destroyList(id))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalled();
@@ -157,7 +154,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.destroyList(id))
+ return expect(boardsStore.destroyList(id))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalled();
@@ -173,7 +170,7 @@ describe('BoardService', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.getIssuesForList(id)).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.getIssuesForList(id)).resolves.toEqual(expectedResponse);
});
it('makes a request to fetch list issues with filter', () => {
@@ -181,13 +178,13 @@ describe('BoardService', () => {
axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
- return expect(service.getIssuesForList(id)).rejects.toThrow();
+ return expect(boardsStore.getIssuesForList(id)).rejects.toThrow();
});
});
@@ -228,7 +225,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -238,7 +235,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -267,7 +264,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.newIssue(id, issue))
+ return expect(boardsStore.newIssue(id, issue))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -277,7 +274,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.newIssue(id, issue))
+ return expect(boardsStore.newIssue(id, issue))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -304,13 +301,13 @@ describe('BoardService', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
- return expect(service.getBacklog(requestParams)).rejects.toThrow();
+ return expect(boardsStore.getBacklog(requestParams)).rejects.toThrow();
});
});
@@ -337,7 +334,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.bulkUpdate(issueIds, extraData))
+ return expect(boardsStore.bulkUpdate(issueIds, extraData))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -347,7 +344,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.bulkUpdate(issueIds, extraData))
+ return expect(boardsStore.bulkUpdate(issueIds, extraData))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -362,13 +359,13 @@ describe('BoardService', () => {
axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(BoardService.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(dummyEndpoint).replyOnce(500);
- return expect(BoardService.getIssueInfo(dummyEndpoint)).rejects.toThrow();
+ return expect(boardsStore.getIssueInfo(dummyEndpoint)).rejects.toThrow();
});
});
@@ -379,7 +376,7 @@ describe('BoardService', () => {
axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
+ return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
expectedResponse,
);
});
@@ -387,7 +384,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
axiosMock.onPost(dummyEndpoint).replyOnce(500);
- return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
+ return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
});
});
@@ -398,13 +395,13 @@ describe('BoardService', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.allBoards()).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.allBoards()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
- return expect(service.allBoards()).rejects.toThrow();
+ return expect(boardsStore.allBoards()).rejects.toThrow();
});
});
@@ -415,13 +412,13 @@ describe('BoardService', () => {
axiosMock.onGet(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.recentBoards()).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.recentBoards()).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onGet(url).replyOnce(500);
- return expect(service.recentBoards()).rejects.toThrow();
+ return expect(boardsStore.recentBoards()).rejects.toThrow();
});
});
@@ -462,7 +459,7 @@ describe('BoardService', () => {
const expectedResponse = expect.objectContaining({ data: dummyResponse });
return expect(
- service.createBoard({
+ boardsStore.createBoard({
...board,
id,
}),
@@ -477,7 +474,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([500]);
return expect(
- service.createBoard({
+ boardsStore.createBoard({
...board,
id,
}),
@@ -513,7 +510,7 @@ describe('BoardService', () => {
requestSpy.mockReturnValue([200, dummyResponse]);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.createBoard(board))
+ return expect(boardsStore.createBoard(board))
.resolves.toEqual(expectedResponse)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -523,7 +520,7 @@ describe('BoardService', () => {
it('fails for error response', () => {
requestSpy.mockReturnValue([500]);
- return expect(service.createBoard(board))
+ return expect(boardsStore.createBoard(board))
.rejects.toThrow()
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
@@ -540,13 +537,13 @@ describe('BoardService', () => {
axiosMock.onDelete(url).replyOnce(200, dummyResponse);
const expectedResponse = expect.objectContaining({ data: dummyResponse });
- return expect(service.deleteBoard({ id })).resolves.toEqual(expectedResponse);
+ return expect(boardsStore.deleteBoard({ id })).resolves.toEqual(expectedResponse);
});
it('fails for error response', () => {
axiosMock.onDelete(url).replyOnce(500);
- return expect(service.deleteBoard({ id })).rejects.toThrow();
+ return expect(boardsStore.deleteBoard({ id })).rejects.toThrow();
});
});
});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 28ab2f2d6c4..99869c46f3f 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,8 +1,7 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
-import { membersBeforeSave } from '~/gfm_auto_complete';
-import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
import 'jquery.caret';
import 'at.js';
diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index 3716b349210..3c960adb698 100644
--- a/spec/frontend/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -1,13 +1,13 @@
import $ from 'helpers/jquery';
import AxiosMockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils';
+import { setTestTimeout } from 'helpers/timeout';
+import axios from '~/lib/utils/axios_utils';
import NotesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
-import { setTestTimeout } from 'helpers/timeout';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import * as mockData from '../../notes/mock_data';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -77,6 +77,8 @@ describe('note_app', () => {
describe('set data', () => {
beforeEach(() => {
+ setFixtures('<div class="js-discussions-count"></div>');
+
axiosMock.onAny().reply(200, []);
wrapper = mountComponent();
return waitForDiscussionsRequest();
@@ -97,6 +99,10 @@ describe('note_app', () => {
it('should fetch discussions', () => {
expect(store.state.discussions).toEqual([]);
});
+
+ it('updates discussions badge', () => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('0');
+ });
});
describe('render', () => {
@@ -161,6 +167,7 @@ describe('note_app', () => {
describe('while fetching data', () => {
beforeEach(() => {
+ setFixtures('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(200, []);
wrapper = mountComponent();
});
@@ -177,6 +184,10 @@ describe('note_app', () => {
'Write a comment or drag your files here…',
);
});
+
+ it('should not update discussions badge (it should be blank)', () => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('');
+ });
});
describe('update note', () => {
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index bef6fbe3d5f..18c94602596 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -122,13 +122,13 @@ describe SearchHelper do
it 'uses the correct singular label' do
collection = Kaminari.paginate_array([:foo]).page(1).per(10)
- expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for \"foo\"")
+ expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for<span>&nbsp;<code>foo</code>&nbsp;</span>")
end
it 'uses the correct plural label' do
collection = Kaminari.paginate_array([:foo] * 23).page(1).per(10)
- expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for \"foo\"")
+ expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for<span>&nbsp;<code>foo</code>&nbsp;</span>")
end
end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index bcc2bd71da1..1aab01281c6 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -27,7 +27,7 @@ describe WikiHelper do
let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort" }
def expected_link(sort, direction, icon_class)
- path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}"
+ path = "/#{project.full_path}/-/wikis/pages?direction=#{direction}&sort=#{sort}"
helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do
helper.sprite_icon("sort-#{icon_class}", size: 16)
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 292fa745fe8..278930789e1 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -8,7 +8,6 @@ import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 890a47c189a..181e7af7451 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -5,7 +5,6 @@ import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
import { setMockEndpoints } from './mock_data';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 1fcd7958bc5..374e7d76b1c 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -10,7 +10,6 @@ import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index ff34dafa660..ba999adb2a9 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -222,6 +222,7 @@ export default {
plain_diff_path: '/root/acets-app/merge_requests/22.diff',
merge_request_basic_path: '/root/acets-app/merge_requests/22.json?serializer=basic',
merge_request_widget_path: '/root/acets-app/merge_requests/22/widget.json',
+ merge_request_cached_widget_path: '/cached.json',
merge_check_path: '/root/acets-app/merge_requests/22/merge_check',
ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status',
project_archived: false,
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 604b21e77fe..36e4ce7177f 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
@@ -17,6 +19,7 @@ const returnPromise = data =>
describe('mrWidgetOptions', () => {
let vm;
+ let mock;
let MrWidgetOptions;
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
@@ -25,6 +28,13 @@ describe('mrWidgetOptions', () => {
// Prevent component mounting
delete mrWidgetOptions.el;
+ gl.mrWidgetData = { ...mockData };
+ gon.features = { asyncMrWidget: true };
+
+ mock = new MockAdapter(axios);
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [200, { ...mockData }]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, { ...mockData }]);
+
MrWidgetOptions = Vue.extend(mrWidgetOptions);
vm = mountComponent(MrWidgetOptions, {
mrData: { ...mockData },
@@ -33,6 +43,9 @@ describe('mrWidgetOptions', () => {
afterEach(() => {
vm.$destroy();
+ mock.restore();
+ gl.mrWidgetData = {};
+ gon.features = {};
});
describe('data', () => {
@@ -308,59 +321,61 @@ describe('mrWidgetOptions', () => {
});
describe('bindEventHubListeners', () => {
- it('should bind eventHub listeners', () => {
+ it('should bind eventHub listeners', done => {
spyOn(vm, 'checkStatus').and.returnValue(() => {});
spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
spyOn(vm, 'fetchActionsContent');
spyOn(vm.mr, 'setData');
spyOn(vm, 'resumePolling');
spyOn(vm, 'stopPolling');
- spyOn(eventHub, '$on');
+ spyOn(eventHub, '$on').and.callThrough();
- vm.bindEventHubListeners();
+ setTimeout(() => {
+ eventHub.$emit('SetBranchRemoveFlag', ['flag']);
- eventHub.$emit('SetBranchRemoveFlag', ['flag']);
+ expect(vm.mr.isRemovingSourceBranch).toEqual('flag');
- expect(vm.mr.isRemovingSourceBranch).toEqual('flag');
+ eventHub.$emit('FailedToMerge');
- eventHub.$emit('FailedToMerge');
+ expect(vm.mr.state).toEqual('failedToMerge');
- expect(vm.mr.state).toEqual('failedToMerge');
+ eventHub.$emit('UpdateWidgetData', mockData);
- eventHub.$emit('UpdateWidgetData', mockData);
+ expect(vm.mr.setData).toHaveBeenCalledWith(mockData);
- expect(vm.mr.setData).toHaveBeenCalledWith(mockData);
+ eventHub.$emit('EnablePolling');
- eventHub.$emit('EnablePolling');
+ expect(vm.resumePolling).toHaveBeenCalled();
- expect(vm.resumePolling).toHaveBeenCalled();
+ eventHub.$emit('DisablePolling');
- eventHub.$emit('DisablePolling');
+ expect(vm.stopPolling).toHaveBeenCalled();
- expect(vm.stopPolling).toHaveBeenCalled();
+ const listenersWithServiceRequest = {
+ MRWidgetUpdateRequested: true,
+ FetchActionsContent: true,
+ };
- const listenersWithServiceRequest = {
- MRWidgetUpdateRequested: true,
- FetchActionsContent: true,
- };
+ const allArgs = eventHub.$on.calls.allArgs();
+ allArgs.forEach(params => {
+ const eventName = params[0];
+ const callback = params[1];
- const allArgs = eventHub.$on.calls.allArgs();
- allArgs.forEach(params => {
- const eventName = params[0];
- const callback = params[1];
+ if (listenersWithServiceRequest[eventName]) {
+ listenersWithServiceRequest[eventName] = callback;
+ }
+ });
- if (listenersWithServiceRequest[eventName]) {
- listenersWithServiceRequest[eventName] = callback;
- }
- });
+ listenersWithServiceRequest.MRWidgetUpdateRequested();
- listenersWithServiceRequest.MRWidgetUpdateRequested();
+ expect(vm.checkStatus).toHaveBeenCalled();
- expect(vm.checkStatus).toHaveBeenCalled();
+ listenersWithServiceRequest.FetchActionsContent();
- listenersWithServiceRequest.FetchActionsContent();
+ expect(vm.fetchActionsContent).toHaveBeenCalled();
- expect(vm.fetchActionsContent).toHaveBeenCalled();
+ done();
+ });
});
});
@@ -451,22 +466,30 @@ describe('mrWidgetOptions', () => {
});
describe('resumePolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- spyOn(vm.pollingInterval, 'resume');
+ it('should call stopTimer on pollingInterval', done => {
+ setTimeout(() => {
+ spyOn(vm.pollingInterval, 'resume');
+
+ vm.resumePolling();
- vm.resumePolling();
+ expect(vm.pollingInterval.resume).toHaveBeenCalled();
- expect(vm.pollingInterval.resume).toHaveBeenCalled();
+ done();
+ });
});
});
describe('stopPolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- spyOn(vm.pollingInterval, 'stopTimer');
+ it('should call stopTimer on pollingInterval', done => {
+ setTimeout(() => {
+ spyOn(vm.pollingInterval, 'stopTimer');
+
+ vm.stopPolling();
- vm.stopPolling();
+ expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
- expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
+ done();
+ });
});
});
});
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 26f2b0b0acf..e1814ea403e 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -72,14 +72,14 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "[Page](./page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page\"")
end
it "rewrites file links to be at the scope of the current directory" do
markdown = "[Link to Page](./page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"")
end
end
@@ -88,14 +88,14 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "[Link to Page](../page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page\"")
end
it "rewrites file links to be at the scope of the parent directory" do
markdown = "[Link to Page](../page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page.md\"")
end
end
@@ -104,14 +104,14 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "[Link to Page](./subdirectory/page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page\"")
end
it "rewrites file links to be at the scope of the sub-directory" do
markdown = "[Link to Page](./subdirectory/page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page.md\"")
end
end
@@ -120,35 +120,35 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "[Link to Page](page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"")
end
it 'rewrites non-file links (with spaces) to be at the scope of the wiki root' do
markdown = "[Link to Page](page slug)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page%20slug\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page%20slug\"")
end
it "rewrites file links to be at the scope of the current directory" do
markdown = "[Link to Page](page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"")
end
it 'rewrites links with anchor' do
markdown = '[Link to Header](start-page#title)'
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start-page#title\"")
end
it 'rewrites links (with spaces) with anchor' do
markdown = '[Link to Header](start page#title)'
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start%20page#title\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start%20page#title\"")
end
end
@@ -157,14 +157,14 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "[Link to Page](/page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"")
end
it 'rewrites file links to be at the scope of the wiki root' do
markdown = "[Link to Page](/page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"")
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page.md\"")
end
end
end
@@ -270,28 +270,28 @@ describe Banzai::Pipeline::WikiPipeline do
markdown = "![video_file](video_file_name.mp4)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"')
+ expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/video_file_name.mp4"')
end
it 'rewrites and replaces video links names with white spaces to %20' do
markdown = "![video file](video file name.mp4)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
+ expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/video%20file%20name.mp4"')
end
it 'generates audio html structure' do
markdown = "![audio_file](audio_file_name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio_file_name.wav"')
+ expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio_file_name.wav"')
end
it 'rewrites and replaces audio links names with white spaces to %20' do
markdown = "![audio file](audio file name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
- expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio%20file%20name.wav"')
+ expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio%20file%20name.wav"')
end
end
end
diff --git a/spec/lib/gitlab/chat/command_spec.rb b/spec/lib/gitlab/chat/command_spec.rb
index 46d23ab2b62..f7f344bf786 100644
--- a/spec/lib/gitlab/chat/command_spec.rb
+++ b/spec/lib/gitlab/chat/command_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Chat::Command do
let(:pipeline) { command.create_pipeline }
before do
- stub_repository_ci_yaml_file(sha: project.commit.id)
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
project.add_developer(chat_name.user)
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index a631cd2777b..b81094f8b4a 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
let(:step) { described_class.new(pipeline, command) }
before do
- stub_repository_ci_yaml_file(sha: anything)
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
it 'never breaks the chain' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
new file mode 100644
index 00000000000..7ebe5842fd0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Config::Content do
+ let(:project) { create(:project, ci_config_path: ci_config_path) }
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+ let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project) }
+
+ subject { described_class.new(pipeline, command) }
+
+ describe '#perform!' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_root_config_content: false)
+ end
+
+ context 'when config is defined in a custom path in the repository' do
+ let(:ci_config_path) { 'path/to/config.yml' }
+
+ before do
+ expect(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(pipeline.sha, ci_config_path)
+ .and_return('the-content')
+ end
+
+ it 'returns the content of the YAML file' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'repository_source'
+ expect(command.config_content).to eq('the-content')
+ end
+ end
+
+ context 'when config is defined remotely' do
+ let(:ci_config_path) { 'http://example.com/path/to/ci/config.yml' }
+
+ it 'does not support URLs and default to AutoDevops' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
+ expect(command.config_content).to eq(template.content)
+ end
+ end
+
+ context 'when config is defined in a separate repository' do
+ let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo' }
+
+ it 'does not support YAML from external repository and default to AutoDevops' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
+ expect(command.config_content).to eq(template.content)
+ end
+ end
+
+ context 'when config is defined in the default .gitlab-ci.yml' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(pipeline.sha, '.gitlab-ci.yml')
+ .and_return('the-content')
+ end
+
+ it 'returns the content of the canonical config file' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'repository_source'
+ expect(command.config_content).to eq('the-content')
+ end
+ end
+
+ context 'when config is the Auto-Devops template' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project).to receive(:auto_devops_enabled?).and_return(true)
+ end
+
+ it 'returns the content of AutoDevops template' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
+ expect(command.config_content).to eq(template.content)
+ end
+ end
+
+ context 'when config is not defined anywhere' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project).to receive(:auto_devops_enabled?).and_return(false)
+ end
+
+ it 'builds root config including the auto-devops template' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq('unknown_source')
+ expect(command.config_content).to be_nil
+ expect(pipeline.errors.full_messages).to include('Missing CI config file')
+ end
+ end
+ end
+
+ context 'when config is defined in a custom path in the repository' do
+ let(:ci_config_path) { 'path/to/config.yml' }
+
+ before do
+ expect(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(pipeline.sha, ci_config_path)
+ .and_return('the-content')
+ end
+
+ it 'builds root config including the local custom file' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'repository_source'
+ expect(command.config_content).to eq(<<~EOY)
+ ---
+ include:
+ - local: #{ci_config_path}
+ EOY
+ end
+ end
+
+ context 'when config is defined remotely' do
+ let(:ci_config_path) { 'http://example.com/path/to/ci/config.yml' }
+
+ it 'builds root config including the remote config' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'remote_source'
+ expect(command.config_content).to eq(<<~EOY)
+ ---
+ include:
+ - remote: #{ci_config_path}
+ EOY
+ end
+ end
+
+ context 'when config is defined in a separate repository' do
+ let(:ci_config_path) { 'path/to/.gitlab-ci.yml@another-group/another-repo' }
+
+ it 'builds root config including the path to another repository' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'external_project_source'
+ expect(command.config_content).to eq(<<~EOY)
+ ---
+ include:
+ - project: another-group/another-repo
+ file: path/to/.gitlab-ci.yml
+ EOY
+ end
+ end
+
+ context 'when config is defined in the default .gitlab-ci.yml' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project.repository)
+ .to receive(:gitlab_ci_yml_for)
+ .with(pipeline.sha, '.gitlab-ci.yml')
+ .and_return('the-content')
+ end
+
+ it 'builds root config including the canonical CI config file' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'repository_source'
+ expect(command.config_content).to eq(<<~EOY)
+ ---
+ include:
+ - local: ".gitlab-ci.yml"
+ EOY
+ end
+ end
+
+ context 'when config is the Auto-Devops template' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project).to receive(:auto_devops_enabled?).and_return(true)
+ end
+
+ it 'builds root config including the auto-devops template' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ expect(command.config_content).to eq(<<~EOY)
+ ---
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ EOY
+ end
+ end
+
+ context 'when config is not defined anywhere' do
+ let(:ci_config_path) { nil }
+
+ before do
+ expect(project).to receive(:auto_devops_enabled?).and_return(false)
+ end
+
+ it 'builds root config including the auto-devops template' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq('unknown_source')
+ expect(command.config_content).to be_nil
+ expect(pipeline.errors.full_messages).to include('Missing CI config file')
+ end
+ end
+ end
+end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 31d1d1fd7d1..af23f121bdc 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -28,7 +28,7 @@ describe ProjectWiki do
describe '#web_url' do
it 'returns the full web URL to the wiki' do
- expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}/wikis/home")
+ expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}/-/wikis/home")
end
end
@@ -71,7 +71,7 @@ describe ProjectWiki do
describe "#wiki_base_path" do
it "returns the wiki base path" do
- wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.full_path}/wikis"
+ wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.full_path}/-/wikis"
expect(subject.wiki_base_path).to eq(wiki_base_path)
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index cce52cfc1ca..2b34b64812d 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -384,7 +384,7 @@ describe API::Pipelines do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
+ expect(json_response['message']['base'].first).to eq 'Missing CI config file'
expect(json_response).not_to be_an Array
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index fc562f93406..287db20448a 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -155,17 +155,21 @@ describe 'project routing' do
# DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy
describe Projects::WikisController, 'routing' do
it 'to #pages' do
- expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'to #history' do
- expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ expect(get('/gitlab/gitlabhq/-/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it_behaves_like 'RESTful project resources' do
let(:actions) { [:create, :edit, :show, :destroy] }
let(:controller) { 'wikis' }
+ let(:controller_path) { '/-/wikis' }
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/wikis", "/gitlab/gitlabhq/-/wikis"
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/wikis/home/edit", "/gitlab/gitlabhq/-/wikis/home/edit"
end
# branches_project_repository GET /:project_id/repository/branches(.:format) projects/repositories#branches
diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
new file mode 100644
index 00000000000..685abbf7e6c
--- /dev/null
+++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequestPollCachedWidgetEntity do
+ include ProjectForksHelper
+
+ let(:project) { create :project, :repository }
+ let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user, project: project) }
+
+ subject do
+ described_class.new(resource, request: request).as_json
+ end
+
+ it 'has the latest sha of the target branch' do
+ is_expected.to include(:target_branch_sha)
+ end
+
+ describe 'diverged_commits_count' do
+ context 'when MR open and its diverging' do
+ it 'returns diverged commits count' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
+ diverged_commits_count: 10)
+
+ expect(subject[:diverged_commits_count]).to eq(10)
+ end
+ end
+
+ context 'when MR is not open' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+
+ context 'when MR is not diverging' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+ end
+
+ describe 'diff_head_sha' do
+ before do
+ allow(resource).to receive(:diff_head_sha) { 'sha' }
+ end
+
+ context 'when diff head commit is empty' do
+ it 'returns nil' do
+ allow(resource).to receive(:diff_head_sha) { '' }
+
+ expect(subject[:diff_head_sha]).to be_nil
+ end
+ end
+
+ context 'when diff head commit present' do
+ it 'returns diff head commit short id' do
+ expect(subject[:diff_head_sha]).to eq('sha')
+ end
+ end
+ end
+
+ describe 'metrics' do
+ context 'when metrics record exists with merged data' do
+ before do
+ resource.mark_as_merged!
+ resource.metrics.update!(merged_by: user)
+ end
+
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+
+ it 'returns values from metrics record' do
+ expect(subject.dig(:metrics, :merged_by, :id))
+ .to eq(resource.metrics.merged_by_id)
+ end
+ end
+
+ context 'when metrics record exists with closed data' do
+ before do
+ resource.close!
+ resource.metrics.update!(latest_closed_by: user)
+ end
+
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+
+ it 'returns values from metrics record' do
+ expect(subject.dig(:metrics, :closed_by, :id))
+ .to eq(resource.metrics.latest_closed_by_id)
+ end
+ end
+
+ context 'when metrics does not exists' do
+ before do
+ resource.mark_as_merged!
+ resource.metrics.destroy!
+ resource.reload
+ end
+
+ context 'when events exists' do
+ let!(:closed_event) { create(:event, :closed, project: project, target: resource) }
+ let!(:merge_event) { create(:event, :merged, project: project, target: resource) }
+
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+
+ it 'returns values from events record' do
+ expect(subject.dig(:metrics, :merged_by, :id))
+ .to eq(merge_event.author_id)
+
+ expect(subject.dig(:metrics, :closed_by, :id))
+ .to eq(closed_event.author_id)
+
+ expect(subject.dig(:metrics, :merged_at).to_s)
+ .to eq(merge_event.updated_at.to_s)
+
+ expect(subject.dig(:metrics, :closed_at).to_s)
+ .to eq(closed_event.updated_at.to_s)
+ end
+ end
+
+ context 'when events does not exists' do
+ it 'matches merge request metrics schema' do
+ expect(subject[:metrics].with_indifferent_access)
+ .to match_schema('entities/merge_request_metrics')
+ end
+ end
+ end
+ end
+
+ describe 'commits_without_merge_commits' do
+ def find_matching_commit(short_id)
+ resource.commits.find { |c| c.short_id == short_id }
+ end
+
+ it 'does not include merge commits' do
+ commits_in_widget = subject[:commits_without_merge_commits]
+
+ expect(commits_in_widget.length).to be < resource.commits.length
+ expect(commits_in_widget.length).to eq(resource.commits.without_merge_commits.length)
+ commits_in_widget.each do |c|
+ expect(find_matching_commit(c[:short_id]).merge_commit?).to eq(false)
+ end
+ end
+ end
+
+ describe 'auto merge' do
+ context 'when auto merge is enabled' do
+ let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_truthy
+ end
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:resource) { create(:merge_request) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_falsy
+ end
+ end
+ end
+
+ describe 'attributes for squash commit message' do
+ context 'when merge request is mergeable' do
+ before do
+ stub_const('MergeRequestDiff::COMMITS_SAFE_SIZE', 20)
+ end
+
+ it 'has default_squash_commit_message and commits_without_merge_commits' do
+ expect(subject[:default_squash_commit_message])
+ .to eq(resource.default_squash_commit_message)
+ expect(subject[:commits_without_merge_commits].size).to eq(12)
+ end
+ end
+
+ context 'when merge request is not mergeable' do
+ before do
+ allow(resource).to receive(:mergeable?).and_return(false)
+ end
+
+ it 'does not have default_squash_commit_message and commits_without_merge_commits' do
+ expect(subject[:default_squash_commit_message]).to eq(nil)
+ expect(subject[:commits_without_merge_commits]).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb
new file mode 100644
index 00000000000..ea9052b4046
--- /dev/null
+++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequestPollWidgetEntity do
+ include ProjectForksHelper
+
+ let(:project) { create :project, :repository }
+ let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user, project: project) }
+
+ subject do
+ described_class.new(resource, request: request).as_json
+ end
+
+ it 'has default_merge_commit_message_with_description' do
+ expect(subject[:default_merge_commit_message_with_description])
+ .to eq(resource.default_merge_commit_message(include_description: true))
+ end
+
+ describe 'merge_pipeline' do
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+
+ context 'when is merged' do
+ let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns merge_pipeline' do
+ pipeline.reload
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: request)
+ .as_json
+
+ expect(subject[:merge_pipeline]).to eq(pipeline_payload)
+ end
+
+ context 'when user cannot read pipelines on target project' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'new_blob_path' do
+ context 'when user can push to project' do
+ it 'returns path' do
+ project.add_developer(user)
+
+ expect(subject[:new_blob_path])
+ .to eq("/#{resource.project.full_path}/new/#{resource.source_branch}")
+ end
+ end
+
+ context 'when user cannot push to project' do
+ it 'returns nil' do
+ expect(subject[:new_blob_path]).to be_nil
+ end
+ end
+ end
+
+ describe 'exposed_artifacts_path' do
+ context 'when merge request has exposed artifacts' do
+ before do
+ expect(resource).to receive(:has_exposed_artifacts?).and_return(true)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:exposed_artifacts_path]).to be_present
+ end
+ end
+
+ context 'when merge request has no exposed artifacts' do
+ before do
+ expect(resource).to receive(:has_exposed_artifacts?).and_return(false)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:exposed_artifacts_path]).to be_nil
+ end
+ end
+ end
+
+ describe 'auto merge' do
+ context 'when auto merge is enabled' do
+ let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_strategy]).to eq('merge_when_pipeline_succeeds')
+ end
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:resource) { create(:merge_request) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_strategy]).to be_nil
+ end
+ end
+
+ context 'when head pipeline is running' do
+ before do
+ create(:ci_pipeline, :running, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to eq(%w[merge_when_pipeline_succeeds])
+ end
+ end
+
+ context 'when head pipeline is finished' do
+ before do
+ create(:ci_pipeline, :success, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to be_empty
+ end
+ end
+ end
+
+ describe 'pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
+
+ before do
+ allow_any_instance_of(MergeRequestPresenter).to receive(:can?).and_call_original
+ allow_any_instance_of(MergeRequestPresenter).to receive(:can?).with(user, :read_pipeline, anything).and_return(result)
+ end
+
+ context 'when user has access to pipelines' do
+ let(:result) { true }
+
+ context 'when is up to date' do
+ let(:req) { double('request', current_user: user, project: project) }
+
+ it 'returns pipeline' do
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eq(pipeline_payload)
+ end
+ end
+
+ context 'when is not up to date' do
+ it 'returns nil' do
+ pipeline.update(sha: "not up to date")
+
+ expect(subject[:pipeline]).to eq(nil)
+ end
+ end
+ end
+
+ context 'when user does not have access to pipelines' do
+ let(:result) { false }
+
+ it 'does not have pipeline' do
+ expect(subject[:pipeline]).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 4e79fd32215..22232682be1 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -15,10 +15,6 @@ describe MergeRequestWidgetEntity do
described_class.new(resource, request: request).as_json
end
- it 'has the latest sha of the target branch' do
- is_expected.to include(:target_branch_sha)
- end
-
describe 'source_project_full_path' do
it 'includes the full path of the source project' do
expect(subject[:source_project_full_path]).to be_present
@@ -47,156 +43,6 @@ describe MergeRequestWidgetEntity do
end
end
- describe 'pipeline' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
-
- before do
- allow_any_instance_of(MergeRequestPresenter).to receive(:can?).and_call_original
- allow_any_instance_of(MergeRequestPresenter).to receive(:can?).with(user, :read_pipeline, anything).and_return(result)
- end
-
- context 'when user has access to pipelines' do
- let(:result) { true }
-
- context 'when is up to date' do
- let(:req) { double('request', current_user: user, project: project) }
-
- it 'returns pipeline' do
- pipeline_payload = PipelineDetailsEntity
- .represent(pipeline, request: req)
- .as_json
-
- expect(subject[:pipeline]).to eq(pipeline_payload)
- end
- end
-
- context 'when is not up to date' do
- it 'returns nil' do
- pipeline.update(sha: "not up to date")
-
- expect(subject[:pipeline]).to eq(nil)
- end
- end
- end
-
- context 'when user does not have access to pipelines' do
- let(:result) { false }
-
- it 'does not have pipeline' do
- expect(subject[:pipeline]).to eq(nil)
- end
- end
- end
-
- describe 'merge_pipeline' do
- it 'returns nil' do
- expect(subject[:merge_pipeline]).to be_nil
- end
-
- context 'when is merged' do
- let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
-
- before do
- project.add_maintainer(user)
- end
-
- it 'returns merge_pipeline' do
- pipeline.reload
- pipeline_payload = PipelineDetailsEntity
- .represent(pipeline, request: request)
- .as_json
-
- expect(subject[:merge_pipeline]).to eq(pipeline_payload)
- end
-
- context 'when user cannot read pipelines on target project' do
- before do
- project.add_guest(user)
- end
-
- it 'returns nil' do
- expect(subject[:merge_pipeline]).to be_nil
- end
- end
- end
- end
-
- describe 'metrics' do
- context 'when metrics record exists with merged data' do
- before do
- resource.mark_as_merged!
- resource.metrics.update!(merged_by: user)
- end
-
- it 'matches merge request metrics schema' do
- expect(subject[:metrics].with_indifferent_access)
- .to match_schema('entities/merge_request_metrics')
- end
-
- it 'returns values from metrics record' do
- expect(subject.dig(:metrics, :merged_by, :id))
- .to eq(resource.metrics.merged_by_id)
- end
- end
-
- context 'when metrics record exists with closed data' do
- before do
- resource.close!
- resource.metrics.update!(latest_closed_by: user)
- end
-
- it 'matches merge request metrics schema' do
- expect(subject[:metrics].with_indifferent_access)
- .to match_schema('entities/merge_request_metrics')
- end
-
- it 'returns values from metrics record' do
- expect(subject.dig(:metrics, :closed_by, :id))
- .to eq(resource.metrics.latest_closed_by_id)
- end
- end
-
- context 'when metrics does not exists' do
- before do
- resource.mark_as_merged!
- resource.metrics.destroy!
- resource.reload
- end
-
- context 'when events exists' do
- let!(:closed_event) { create(:event, :closed, project: project, target: resource) }
- let!(:merge_event) { create(:event, :merged, project: project, target: resource) }
-
- it 'matches merge request metrics schema' do
- expect(subject[:metrics].with_indifferent_access)
- .to match_schema('entities/merge_request_metrics')
- end
-
- it 'returns values from events record' do
- expect(subject.dig(:metrics, :merged_by, :id))
- .to eq(merge_event.author_id)
-
- expect(subject.dig(:metrics, :closed_by, :id))
- .to eq(closed_event.author_id)
-
- expect(subject.dig(:metrics, :merged_at).to_s)
- .to eq(merge_event.updated_at.to_s)
-
- expect(subject.dig(:metrics, :closed_at).to_s)
- .to eq(closed_event.updated_at.to_s)
- end
- end
-
- context 'when events does not exists' do
- it 'matches merge request metrics schema' do
- expect(subject[:metrics].with_indifferent_access)
- .to match_schema('entities/merge_request_metrics')
- end
- end
- end
- end
-
it 'has email_patches_path' do
expect(subject[:email_patches_path])
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch")
@@ -207,100 +53,6 @@ describe MergeRequestWidgetEntity do
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff")
end
- it 'has default_merge_commit_message_with_description' do
- expect(subject[:default_merge_commit_message_with_description])
- .to eq(resource.default_merge_commit_message(include_description: true))
- end
-
- describe 'attributes for squash commit message' do
- context 'when merge request is mergeable' do
- before do
- stub_const('MergeRequestDiff::COMMITS_SAFE_SIZE', 20)
- end
-
- it 'has default_squash_commit_message and commits_without_merge_commits' do
- expect(subject[:default_squash_commit_message])
- .to eq(resource.default_squash_commit_message)
- expect(subject[:commits_without_merge_commits].size).to eq(12)
- end
- end
-
- context 'when merge request is not mergeable' do
- before do
- allow(resource).to receive(:mergeable?).and_return(false)
- end
-
- it 'does not have default_squash_commit_message and commits_without_merge_commits' do
- expect(subject[:default_squash_commit_message]).to eq(nil)
- expect(subject[:commits_without_merge_commits]).to eq(nil)
- end
- end
- end
-
- describe 'new_blob_path' do
- context 'when user can push to project' do
- it 'returns path' do
- project.add_developer(user)
-
- expect(subject[:new_blob_path])
- .to eq("/#{resource.project.full_path}/new/#{resource.source_branch}")
- end
- end
-
- context 'when user cannot push to project' do
- it 'returns nil' do
- expect(subject[:new_blob_path]).to be_nil
- end
- end
- end
-
- describe 'diff_head_sha' do
- before do
- allow(resource).to receive(:diff_head_sha) { 'sha' }
- end
-
- context 'when diff head commit is empty' do
- it 'returns nil' do
- allow(resource).to receive(:diff_head_sha) { '' }
-
- expect(subject[:diff_head_sha]).to be_nil
- end
- end
-
- context 'when diff head commit present' do
- it 'returns diff head commit short id' do
- expect(subject[:diff_head_sha]).to eq('sha')
- end
- end
- end
-
- describe 'diverged_commits_count' do
- context 'when MR open and its diverging' do
- it 'returns diverged commits count' do
- allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
- diverged_commits_count: 10)
-
- expect(subject[:diverged_commits_count]).to eq(10)
- end
- end
-
- context 'when MR is not open' do
- it 'returns 0' do
- allow(resource).to receive_messages(open?: false)
-
- expect(subject[:diverged_commits_count]).to be_zero
- end
- end
-
- context 'when MR is not diverging' do
- it 'returns 0' do
- allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
-
- expect(subject[:diverged_commits_count]).to be_zero
- end
- end
- end
-
describe 'when source project is deleted' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project(project) }
@@ -316,88 +68,4 @@ describe MergeRequestWidgetEntity do
expect(entity[:rebase_path]).to be_nil
end
end
-
- describe 'commits_without_merge_commits' do
- def find_matching_commit(short_id)
- resource.commits.find { |c| c.short_id == short_id }
- end
-
- it 'does not include merge commits' do
- commits_in_widget = subject[:commits_without_merge_commits]
-
- expect(commits_in_widget.length).to be < resource.commits.length
- expect(commits_in_widget.length).to eq(resource.commits.without_merge_commits.length)
- commits_in_widget.each do |c|
- expect(find_matching_commit(c[:short_id]).merge_commit?).to eq(false)
- end
- end
- end
-
- describe 'auto merge' do
- context 'when auto merge is enabled' do
- let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
-
- it 'returns auto merge related information' do
- expect(subject[:auto_merge_enabled]).to be_truthy
- expect(subject[:auto_merge_strategy]).to eq('merge_when_pipeline_succeeds')
- end
- end
-
- context 'when auto merge is not enabled' do
- let(:resource) { create(:merge_request) }
-
- it 'returns auto merge related information' do
- expect(subject[:auto_merge_enabled]).to be_falsy
- expect(subject[:auto_merge_strategy]).to be_nil
- end
- end
-
- context 'when head pipeline is running' do
- before do
- create(:ci_pipeline, :running, project: project,
- ref: resource.source_branch,
- sha: resource.diff_head_sha)
- resource.update_head_pipeline
- end
-
- it 'returns available auto merge strategies' do
- expect(subject[:available_auto_merge_strategies]).to eq(%w[merge_when_pipeline_succeeds])
- end
- end
-
- context 'when head pipeline is finished' do
- before do
- create(:ci_pipeline, :success, project: project,
- ref: resource.source_branch,
- sha: resource.diff_head_sha)
- resource.update_head_pipeline
- end
-
- it 'returns available auto merge strategies' do
- expect(subject[:available_auto_merge_strategies]).to be_empty
- end
- end
- end
-
- describe 'exposed_artifacts_path' do
- context 'when merge request has exposed artifacts' do
- before do
- expect(resource).to receive(:has_exposed_artifacts?).and_return(true)
- end
-
- it 'set the path to poll data' do
- expect(subject[:exposed_artifacts_path]).to be_present
- end
- end
-
- context 'when merge request has no exposed artifacts' do
- before do
- expect(resource).to receive(:has_exposed_artifacts?).and_return(false)
- end
-
- it 'set the path to poll data' do
- expect(subject[:exposed_artifacts_path]).to be_nil
- end
- end
- end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8f16d375f03..d450bb12e8c 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -10,7 +10,7 @@ describe Ci::CreatePipelineService do
let(:ref_name) { 'refs/heads/master' }
before do
- stub_repository_ci_yaml_file(sha: anything)
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
describe '#execute' do
@@ -510,7 +510,7 @@ describe Ci::CreatePipelineService do
it 'attaches errors to the pipeline' do
pipeline = execute_service
- expect(pipeline.errors.full_messages).to eq ['Missing .gitlab-ci.yml file']
+ expect(pipeline.errors.full_messages).to eq ['Missing CI config file']
expect(pipeline).not_to be_persisted
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index fe343da7838..ff4b9db8ad9 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -19,24 +19,28 @@ module StubGitlabCalls
end
def stub_ci_pipeline_yaml_file(ci_yaml_content)
- allow_any_instance_of(Repository).to receive(:gitlab_ci_yml_for).and_return(ci_yaml_content)
+ allow_any_instance_of(Repository)
+ .to receive(:gitlab_ci_yml_for)
+ .and_return(ci_yaml_content)
# Ensure we don't hit auto-devops when config not found in repository
unless ci_yaml_content
allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(false)
end
+
+ # Stub the first call to `include:[local: .gitlab-ci.yml]` when
+ # evaluating the CI root config content.
+ if Feature.enabled?(:ci_root_config_content, default_enabled: true)
+ allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
+ .to receive(:content)
+ .and_return(ci_yaml_content)
+ end
end
def stub_pipeline_modified_paths(pipeline, modified_paths)
allow(pipeline).to receive(:modified_paths).and_return(modified_paths)
end
- def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml')
- allow_any_instance_of(Repository)
- .to receive(:gitlab_ci_yml_for).with(sha, path)
- .and_return(gitlab_ci_yaml)
- end
-
def stub_ci_builds_disabled
allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
end
diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb
index 177ade3b700..628d2e10f93 100644
--- a/spec/views/search/_results.html.haml_spec.rb
+++ b/spec/views/search/_results.html.haml_spec.rb
@@ -16,7 +16,7 @@ describe 'search/_results' do
it 'displays the page size' do
render
- expect(rendered).to have_content('Showing 1 - 2 of 3 issues for "foo"')
+ expect(rendered).to have_content('Showing 1 - 2 of 3 issues for foo')
end
context 'when search results do not have a count' do