summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--CHANGELOG.md7
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue97
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js32
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/controllers/admin/applications_controller.rb4
-rw-r--r--app/controllers/concerns/issuable_collections.rb28
-rw-r--r--app/controllers/concerns/paginated_collection.rb19
-rw-r--r--app/controllers/dashboard/snippets_controller.rb17
-rw-r--r--app/controllers/dashboard/todos_controller.rb32
-rw-r--r--app/controllers/explore/snippets_controller.rb13
-rw-r--r--app/controllers/projects/forks_controller.rb15
-rw-r--r--app/controllers/projects/snippets_controller.rb19
-rw-r--r--app/controllers/snippets_controller.rb10
-rw-r--r--app/controllers/users_controller.rb12
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/views/admin/applications/index.html.haml3
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml2
-rw-r--r--app/views/graphiql/rails/editors/show.html.erb18
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/shared/snippets/_list.html.haml7
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--changelogs/unreleased/65940-run-pipeline.yml5
-rw-r--r--changelogs/unreleased/65988-optimize-snippet-listings.yml5
-rw-r--r--changelogs/unreleased/security-12-3-bump-pages.yml5
-rw-r--r--changelogs/unreleased/sh-fix-oauth-application-page.yml5
-rw-r--r--db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb17
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/geo/replication/docker_registry.md112
-rw-r--r--doc/administration/geo/replication/index.md2
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md6
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md4
-rw-r--r--doc/administration/troubleshooting/linux_cheat_sheet.md2
-rw-r--r--doc/administration/troubleshooting/test_environments.md4
-rw-r--r--doc/api/merge_requests.md60
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/examples/code_quality.md117
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/subscriptions/index.md2
-rw-r--r--doc/topics/autodevops/index.md10
-rw-r--r--doc/user/discussions/index.md3
-rw-r--r--doc/user/markdown.md62
-rw-r--r--doc/user/project/merge_requests/code_quality.md113
-rw-r--r--lib/api/merge_requests.rb20
-rw-r--r--lib/gitlab/issuable_metadata.rb4
-rw-r--r--lib/gitlab/noteable_metadata.rb33
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/main/login.rb6
-rw-r--r--qa/qa/page/merge_request/show.rb6
-rw-r--r--qa/qa/page/project/sub_menus/repository.rb2
-rw-r--r--qa/qa/resource/api_fabricator.rb16
-rw-r--r--qa/qa/resource/base.rb12
-rw-r--r--qa/qa/resource/branch.rb12
-rw-r--r--qa/qa/resource/merge_request.rb4
-rw-r--r--qa/qa/resource/project_member.rb35
-rw-r--r--qa/qa/support/api.rb1
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb10
-rw-r--r--spec/controllers/dashboard/snippets_controller_spec.rb21
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb24
-rw-r--r--spec/controllers/explore/snippets_controller_spec.rb15
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb61
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb54
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb32
-rw-r--r--spec/controllers/snippets_controller_spec.rb9
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb32
-rw-r--r--spec/features/projects/fork_spec.rb1
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js167
-rw-r--r--spec/lib/gitlab/noteable_metadata_spec.rb29
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/models/oauth_access_token_spec.rb28
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/import_github_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb64
-rw-r--r--spec/requests/api/project_events_spec.rb2
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb4
-rw-r--r--spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb30
93 files changed, 1304 insertions, 410 deletions
diff --git a/.dockerignore b/.dockerignore
index 4f5b33de167..d5568619169 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,6 +10,7 @@
# - ./INSTALLATION_TYPE
# - ./VERSION
+/.git/
/app/
/bin/
/builds/
@@ -69,6 +70,7 @@
/locale/
/log/
/modules/
+/node_modules/
/plugins/
/public/
/rubocop/
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 8e612243371..69af12b5108 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -77,7 +77,6 @@ schedule:review-build-cng:
review-deploy:
extends: .review-base
allow_failure: true
- retry: 1
stage: review
needs: ["review-build-cng"]
variables:
@@ -147,7 +146,6 @@ review-cleanup-failed-deployment:
extends:
- .review-docker
- .review-only
- retry: 2
stage: qa
variables:
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a432b091c7e..e379c23ee3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.2.5
+
+### Security (1 change)
+
+- Upgrade pages to 1.7.2.
+
+
## 12.2.4
### Fixed (7 changes)
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 27f9cd322bb..a8fdfda1c78 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.8.0
+1.8.1
diff --git a/Gemfile b/Gemfile
index ac848cce5e8..3ead3cb5f36 100644
--- a/Gemfile
+++ b/Gemfile
@@ -84,7 +84,9 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.9.11'
-gem 'graphiql-rails', '~> 1.4.10'
+# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 will be released
+# https://gitlab.com/gitlab-org/gitlab-ce/issues/67263
+gem 'graphiql-rails', '~> 1.7.0'
gem 'apollo_upload_server', '~> 2.0.0.beta3'
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
diff --git a/Gemfile.lock b/Gemfile.lock
index 48053e5740e..301b54f9a9f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -398,7 +398,7 @@ GEM
rake (~> 12)
grape_logging (1.7.0)
grape
- graphiql-rails (1.4.10)
+ graphiql-rails (1.7.0)
railties
sprockets-rails
graphql (1.9.11)
@@ -1142,7 +1142,7 @@ DEPENDENCIES
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.1)
grape_logging (~> 1.7)
- graphiql-rails (~> 1.4.10)
+ graphiql-rails (~> 1.7.0)
graphql (~> 1.9.11)
graphql-docs (~> 1.6.0)
grpc (~> 1.19.0)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1d97ad5ec11..992c5e5e330 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics',
group(groupId, callback) {
@@ -371,6 +372,14 @@ const Api = {
});
},
+ postMergeRequestPipeline(id, { mergeRequestId }) {
+ const url = Api.buildUrl(this.mergeRequestsPipeline)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':merge_request_iid', mergeRequestId);
+
+ return axios.post(url);
+ },
+
releases(id) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 4890f99e9d1..e5b030d4900 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,14 +1,19 @@
<script>
-import PipelinesService from '../../pipelines/services/pipelines_service';
-import PipelineStore from '../../pipelines/stores/pipelines_store';
-import pipelinesMixin from '../../pipelines/mixins/pipelines';
-import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
-import { getParameterByName } from '../../lib/utils/common_utils';
-import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import PipelinesService from '~/pipelines/services/pipelines_service';
+import PipelineStore from '~/pipelines/stores/pipelines_store';
+import pipelinesMixin from '~/pipelines/mixins/pipelines';
+import eventHub from '~/pipelines/event_hub';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
+import bp from '~/breakpoints';
export default {
components: {
TablePagination,
+ GlButton,
+ GlLoadingIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
@@ -33,6 +38,21 @@ export default {
required: false,
default: 'child',
},
+ canRunPipeline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ mergeRequestId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
@@ -53,6 +73,41 @@ export default {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
+ /**
+ * The Run Pipeline button can only be rendered when:
+ * - In MR view - we use `canRunPipeline` for that purpose
+ * - If the latest pipeline has the `detached_merge_request_pipeline` flag
+ *
+ * @returns {Boolean}
+ */
+ canRenderPipelineButton() {
+ return this.canRunPipeline && this.latestPipelineDetachedFlag;
+ },
+ /**
+ * Checks if either `detached_merge_request_pipeline` or
+ * `merge_request_pipeline` are tru in the first
+ * object in the pipelines array.
+ *
+ * @returns {Boolean}
+ */
+ latestPipelineDetachedFlag() {
+ const latest = this.state.pipelines[0];
+ return (
+ latest &&
+ latest.flags &&
+ (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline)
+ );
+ },
+ /**
+ * When we are on Desktop and the button is visible
+ * we need to add a negative margin to the table
+ * to make it inline with the button
+ *
+ * @returns {Boolean}
+ */
+ shouldAddNegativeMargin() {
+ return this.canRenderPipelineButton && bp.isDesktop();
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -77,6 +132,22 @@ export default {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
+ /**
+ * When the user clicks on the Run Pipeline button
+ * we need to make a post request and
+ * to update the table content once the request is finished.
+ *
+ * We are emitting an event through the eventHub using the old pattern
+ * to make use of the code in mixins/pipelines.js that handles all the
+ * table events
+ *
+ */
+ onClickRunPipeline() {
+ eventHub.$emit('runMergeRequestPipeline', {
+ projectId: this.projectId,
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
},
};
</script>
@@ -99,11 +170,25 @@ export default {
/>
<div v-else-if="shouldRenderTable" class="table-holder">
+ <div v-if="canRenderPipelineButton" class="nav justify-content-end">
+ <gl-button
+ v-if="canRenderPipelineButton"
+ variant="success"
+ class="js-run-mr-pipeline prepend-top-10 btn-wide-on-xs"
+ :disabled="state.isRunningMergeRequestPipeline"
+ @click="onClickRunPipeline"
+ >
+ <gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline />
+ {{ s__('Pipelines|Run Pipeline') }}
+ </gl-button>
+ </div>
+
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :class="{ 'negative-margin-top': shouldAddNegativeMargin }"
/>
</div>
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b6868e63716..52674107df2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -333,7 +333,8 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const { CommitPipelinesTable } = gl;
+ const { CommitPipelinesTable, mrWidgetData } = gl;
+
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
@@ -341,6 +342,9 @@ export default class MergeRequestTabs {
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ canRunPipeline: true,
+ projectId: pipelineTableViewEl.dataset.projectId,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
},
}).$mount();
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 126a9a47a2b..876b30299fb 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '../../locale';
-import Flash from '../../flash';
+import createFlash from '../../flash';
import Poll from '../../lib/utils/poll';
import EmptyState from '../components/empty_state.vue';
import SvgBlankState from '../components/blank_state.vue';
@@ -62,6 +62,7 @@ export default {
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
@@ -69,6 +70,7 @@ export default {
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
this.poll.stop();
@@ -110,7 +112,7 @@ export default {
// Stop polling
this.poll.stop();
// Restarting the poll also makes an initial request
- this.poll.restart();
+ return this.poll.restart();
},
fetchPipelines() {
if (!this.isMakingRequest) {
@@ -156,7 +158,31 @@ export default {
this.service
.postAction(endpoint)
.then(() => this.updateTable())
- .catch(() => Flash(__('An error occurred while making the request.')));
+ .catch(() => createFlash(__('An error occurred while making the request.')));
+ },
+
+ /**
+ * When the user clicks on the run pipeline button
+ * we toggle the state of the button to be disabled
+ *
+ * Once the post request has finished, we fetch the
+ * pipelines again to show the most recent data
+ *
+ * Once the pipeline has been updated, we toggle back the
+ * loading state and re-enable the run pipeline button
+ */
+ runMergeRequestPipeline(options) {
+ this.store.toggleIsRunningPipeline(true);
+
+ this.service
+ .runMRPipeline(options)
+ .then(() => this.updateTable())
+ .catch(() => {
+ createFlash(
+ __('An error occurred while trying to run a new pipeline for this Merge Request.'),
+ );
+ })
+ .finally(() => this.store.toggleIsRunningPipeline(false));
},
},
};
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 8317d3f4510..3c755db23dc 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,4 +1,5 @@
import axios from '../../lib/utils/axios_utils';
+import Api from '~/api';
export default class PipelinesService {
/**
@@ -39,4 +40,9 @@ export default class PipelinesService {
postAction(endpoint) {
return axios.post(`${endpoint}.json`);
}
+
+ // eslint-disable-next-line class-methods-use-this
+ runMRPipeline({ projectId, mergeRequestId }) {
+ return Api.postMergeRequestPipeline(projectId, { mergeRequestId });
+ }
}
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 651251d2623..a4bbada89c8 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -7,6 +7,9 @@ export default class PipelinesStore {
this.state.pipelines = [];
this.state.count = {};
this.state.pageInfo = {};
+
+ // Used in MR Pipelines tab
+ this.state.isRunningMergeRequestPipeline = false;
}
storePipelines(pipelines = []) {
@@ -29,4 +32,13 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo;
}
+
+ /**
+ * Toggles the isRunningPipeline flag
+ *
+ * @param {Boolean} value
+ */
+ toggleIsRunningPipeline(value = false) {
+ this.state.isRunningMergeRequestPipeline = value;
+ }
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 15a779dde1d..faa0a9909d5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -726,6 +726,7 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
+$pipelines-table-header-height: 40px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index d4bd5b1b7dc..cda6c9ce0cc 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -26,6 +26,10 @@
}
.pipelines {
+ .negative-margin-top {
+ margin-top: -$pipelines-table-header-height;
+ }
+
.stage {
max-width: 90px;
width: 90px;
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 3648c8be426..22e629ccf59 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -7,7 +7,9 @@ class Admin::ApplicationsController < Admin::ApplicationController
before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
- @applications = ApplicationsFinder.new.execute
+ applications = ApplicationsFinder.new.execute
+ @applications = Kaminari.paginate_array(applications).page(params[:page])
+ @application_counts = OauthAccessToken.distinct_resource_owner_counts(@applications)
end
def show
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 8ea77b994de..88044cf7557 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,6 +2,7 @@
module IssuableCollections
extend ActiveSupport::Concern
+ include PaginatedCollection
include SortingHelper
include SortingPreference
include Gitlab::IssuableMetadata
@@ -17,8 +18,11 @@ module IssuableCollections
def set_issuables_index
@issuables = issuables_collection
- set_pagination
- return if redirect_out_of_range(@total_pages)
+ unless pagination_disabled?
+ set_pagination
+
+ return if redirect_out_of_range(@issuables, @total_pages)
+ end
if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -38,12 +42,10 @@ module IssuableCollections
end
def set_pagination
- return if pagination_disabled?
-
@issuables = @issuables.page(params[:page])
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
@issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
- @total_pages = issuable_page_count
+ @total_pages = issuable_page_count(@issuables)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -57,20 +59,8 @@ module IssuableCollections
end
# rubocop: enable CodeReuse/ActiveRecord
- def redirect_out_of_range(total_pages)
- return false if total_pages.nil? || total_pages.zero?
-
- out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- if out_of_range
- redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
- end
-
- out_of_range
- end
-
- def issuable_page_count
- page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def issuable_page_count(relation)
+ page_count_for_relation(relation, finder.row_count)
end
def page_count_for_relation(relation, row_count)
diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb
new file mode 100644
index 00000000000..be84215a9e2
--- /dev/null
+++ b/app/controllers/concerns/paginated_collection.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module PaginatedCollection
+ extend ActiveSupport::Concern
+
+ private
+
+ def redirect_out_of_range(collection, total_pages = collection.total_pages)
+ return false if total_pages.zero?
+
+ out_of_range = collection.current_page > total_pages
+
+ if out_of_range
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
+ end
+
+ out_of_range
+ end
+end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 161c22046f9..6feade3df03 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,14 +1,19 @@
# frozen_string_literal: true
class Dashboard::SnippetsController < Dashboard::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
skip_cross_project_access_check :index
def index
- @snippets = SnippetsFinder.new(
- current_user,
- author: current_user,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: current_user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 8f6fcb362d2..940d1482611 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
+ include PaginatedCollection
before_action :authorize_read_project!, only: :index
before_action :authorize_read_group!, only: :index
@@ -12,7 +13,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@todos = @todos.page(params[:page])
@todos = @todos.with_entity_associations
- return if redirect_out_of_range(@todos)
+ return if redirect_out_of_range(@todos, todos_page_count(@todos))
end
def destroy
@@ -82,28 +83,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController
}
end
- def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def redirect_out_of_range(todos)
- total_pages =
- if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count.to_f / todos.limit_value).ceil
- else
- todos.total_pages
- end
-
- return false if total_pages.zero?
-
- out_of_range = todos.current_page > total_pages
-
- if out_of_range
- redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
+ def todos_page_count(todos)
+ if todo_params.except(:sort, :page).empty? # rubocop: disable CodeReuse/ActiveRecord
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
+ else
+ todos.total_pages
end
+ end
- out_of_range
+ def todo_params
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 76ed142c939..d4c6aae2ca8 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,8 +1,17 @@
# frozen_string_literal: true
class Explore::SnippetsController < Explore::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
def index
- @snippets = SnippetsFinder.new(current_user).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user)
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ac1c4bc7fd3..1bb21857dcf 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -2,6 +2,7 @@
class Projects::ForksController < Projects::ApplicationController
include ContinueParams
+ include RendersMemberAccess
# Authorize
before_action :whitelist_query_limiting, only: [:create]
@@ -11,14 +12,16 @@ class Projects::ForksController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- base_query = project.forks.includes(:creator)
+ @total_forks_count = project.forks.size
+ @public_forks_count = project.forks.public_only.size
+ @private_forks_count = @total_forks_count - project.forks.public_and_internal_only.size
+ @internal_forks_count = @total_forks_count - @public_forks_count - @private_forks_count
- forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
- @total_forks_count = base_query.size
- @private_forks_count = @total_forks_count - forks.size
- @public_forks_count = @total_forks_count - @private_forks_count
+ @forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
+ @forks = @forks.includes(:route, :creator, :group, namespace: [:route, :owner])
+ .page(params[:page])
- @forks = forks.page(params[:page])
+ prepare_projects_for_rendering(@forks)
respond_to do |format|
format.html
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 59f948959d6..dbd11c8ddc8 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -6,6 +6,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -28,15 +30,14 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new(
- current_user,
- project: @project,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
- if @snippets.out_of_range? && @snippets.total_pages != 0
- redirect_to project_snippets_path(@project, page: @snippets.total_pages)
- end
+ @snippets = SnippetsFinder.new(current_user, project: @project, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def new
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 869655e9550..5805d068e21 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -7,6 +7,8 @@ class SnippetsController < ApplicationController
include SnippetsActions
include RendersBlob
include PreviewMarkdown
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -32,7 +34,13 @@ class SnippetsController < ApplicationController
@user = UserFinder.new(params[:username]).find_by_username!
@snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
- .execute.page(params[:page])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
render 'index'
else
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 91e0efcf45f..e38d4073de3 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -4,6 +4,7 @@ class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
+ include Gitlab::NoteableMetadata
requires_cross_project_access show: false,
groups: false,
@@ -165,11 +166,12 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new(
- current_user,
- author: user,
- scope: params[:scope]
- ).execute.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def build_canonical_path(user)
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 6a44bc7c401..b3e4df730b4 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,6 +3,10 @@
module Noteable
extend ActiveSupport::Concern
+ # This object is used to gather noteable meta data for list displays
+ # avoiding n+1 queries and improving performance.
+ NoteableMeta = Struct.new(:user_notes_count)
+
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 0aa920fa828..9789d8ed62b 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -6,6 +6,8 @@ class OauthAccessToken < Doorkeeper::AccessToken
alias_attribute :user, :resource_owner
+ scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
+
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 00931457344..b2fca65b9e0 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -55,6 +55,7 @@ class Snippet < ApplicationRecord
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
+ scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) }
participant :author
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 2cdf98075d1..758d722cc63 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -19,7 +19,8 @@
%tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
- %td= application.access_tokens.map(&:resource_owner_id).uniq.count
+ %td= @application_counts[application.id].to_i
%td= application.trusted? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= render 'delete_form', application: application
+= paginate @applications, theme: 'gitlab'
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 5507f12b73b..a6acf948ed4 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,5 +1,5 @@
- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank'
+ anchor: 'add-existing-kubernetes-cluster'), target: '_blank'
- rbac_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
diff --git a/app/views/clusters/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 749177fa6c1..3b9ceaa2b8a 100644
--- a/app/views/clusters/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'add-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/graphiql/rails/editors/show.html.erb b/app/views/graphiql/rails/editors/show.html.erb
new file mode 100644
index 00000000000..abb1ed0e772
--- /dev/null
+++ b/app/views/graphiql/rails/editors/show.html.erb
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= GraphiQL::Rails.config.title || 'GraphiQL' %></title>
+
+ <%= stylesheet_link_tag("graphiql/rails/application") %>
+ <%= javascript_include_tag("graphiql/rails/application", nonce: true) %>
+ </head>
+ <body>
+ <%= content_tag :div, 'Loading...', id: 'graphiql-container', data: {
+ graphql_endpoint_path: graphql_endpoint_path,
+ initial_query: GraphiQL::Rails.config.initial_query,
+ logo: GraphiQL::Rails.config.logo,
+ headers: GraphiQL::Rails.config.resolve_headers(self),
+ query_params: GraphiQL::Rails.config.query_params
+ } %>
+ </body>
+</html>
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 68b35072f26..81c354f1c8f 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -5,4 +5,5 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
+ "project-id": @project.id,
} }
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 0397a7034c7..8384561891a 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,6 +1,6 @@
.top-area
.nav-text
- - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ - full_count_title = "#{@public_forks_count} public, #{@internal_forks_count} internal, and #{@private_forks_count} private"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index bb05658c719..d70a1631010 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -42,12 +42,6 @@
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
-
- - if @private_forks_count && @private_forks_count > 0
- %li.project-row.private-forks-notice
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(@private_forks_count, 'private fork')
- %span &nbsp;you have no access to.
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- if @contributed_projects
diff --git a/app/views/shared/snippets/_list.html.haml b/app/views/shared/snippets/_list.html.haml
index 5d2152eb411..766f48fff3d 100644
--- a/app/views/shared/snippets/_list.html.haml
+++ b/app/views/shared/snippets/_list.html.haml
@@ -1,12 +1,11 @@
- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
-- if @snippets.exists?
+- if @snippets.to_a.empty?
+ .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
+- else
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
= paginate @snippets, theme: 'gitlab', remote: remote
-
-- else
- .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 42af97bc6af..0ef626868a2 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,4 +1,5 @@
- link_project = local_assigns.fetch(:link_project, false)
+- notes_count = @noteable_meta_data[snippet.id].user_notes_count
%li.snippet-row
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
@@ -12,10 +13,9 @@
%ul.controls
%li
- - note_count = snippet.notes.user.count
- = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+ = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
= icon('comments')
- = note_count
+ = notes_count
%li
%span.sr-only
= visibility_level_label(snippet.visibility_level)
diff --git a/changelogs/unreleased/65940-run-pipeline.yml b/changelogs/unreleased/65940-run-pipeline.yml
new file mode 100644
index 00000000000..c0e89a19373
--- /dev/null
+++ b/changelogs/unreleased/65940-run-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Run Pipeline button & API for MR Pipelines
+merge_request: 31722
+author:
+type: added
diff --git a/changelogs/unreleased/65988-optimize-snippet-listings.yml b/changelogs/unreleased/65988-optimize-snippet-listings.yml
new file mode 100644
index 00000000000..186a83cf9f3
--- /dev/null
+++ b/changelogs/unreleased/65988-optimize-snippet-listings.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize queries for snippet listings
+merge_request: 32576
+author:
+type: performance
diff --git a/changelogs/unreleased/security-12-3-bump-pages.yml b/changelogs/unreleased/security-12-3-bump-pages.yml
new file mode 100644
index 00000000000..cf602e3cf48
--- /dev/null
+++ b/changelogs/unreleased/security-12-3-bump-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade pages to 1.8.1
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-oauth-application-page.yml b/changelogs/unreleased/sh-fix-oauth-application-page.yml
new file mode 100644
index 00000000000..9ac78db2c79
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-oauth-application-page.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize /admin/applications so that it does not timeout
+merge_request: 32852
+author:
+type: performance
diff --git a/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb b/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb
new file mode 100644
index 00000000000..78f7c0ecf0f
--- /dev/null
+++ b/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnApplicationIdOnOauthAccessTokens < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :oauth_access_tokens, :application_id
+ end
+
+ def down
+ remove_concurrent_index :oauth_access_tokens, :application_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 70f3a42e7f8..14ce50b0619 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_09_05_223900) do
+ActiveRecord::Schema.define(version: 2019_09_10_000130) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -2390,6 +2390,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.datetime "revoked_at"
t.datetime "created_at", null: false
t.string "scopes"
+ t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id"
t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
diff --git a/doc/administration/geo/replication/docker_registry.md b/doc/administration/geo/replication/docker_registry.md
index d5c2d2362b1..b326717205e 100644
--- a/doc/administration/geo/replication/docker_registry.md
+++ b/doc/administration/geo/replication/docker_registry.md
@@ -1,23 +1,113 @@
# Docker Registry for a secondary node **(PREMIUM ONLY)**
-You can set up a [Docker Registry] on your
+You can set up a [Docker Registry](https://docs.docker.com/registry/) on your
**secondary** Geo node that mirrors the one on the **primary** Geo node.
## Storage support
-CAUTION: **Warning:**
-If you use [local storage][registry-storage]
-for the Container Registry you **cannot** replicate it to a **secondary** node.
-
Docker Registry currently supports a few types of storages. If you choose a
distributed storage (`azure`, `gcs`, `s3`, `swift`, or `oss`) for your Docker
Registry on the **primary** node, you can use the same storage for a **secondary**
Docker Registry as well. For more information, read the
-[Load balancing considerations][registry-load-balancing]
+[Load balancing considerations](https://docs.docker.com/registry/deploying/#load-balancing-considerations)
when deploying the Registry, and how to set up the storage driver for GitLab's
-integrated [Container Registry][registry-storage].
+integrated [Container Registry](../../container_registry.md#container-registry-storage-driver).
+
+## Replicating Docker Registry
+
+You can enable a storage-agnostic replication so it
+can be used for cloud or local storages. Whenever a new image is pushed to the
+primary node, each **secondary** node will pull it to its own container
+repository.
+
+To configure Docker Registry replication:
+
+1. Configure the [**primary** node](#configure-primary-node).
+1. Configure the [**secondary** node](#configure-secondary-node).
+1. Verify Docker Registry [replication](#verify-replication).
+
+### Configure **primary** node
+
+Make sure that you have Container Registry set up and working on
+the **primary** node before following the next steps.
+
+We need to make Docker Registry send notification events to the
+**primary** node.
+
+1. SSH into your GitLab **primary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['notifications'] = [
+ {
+ 'name' => 'geo_event',
+ 'url' => 'https://example.com/api/v4/container_registry_event/events',
+ 'timeout' => '500ms',
+ 'threshold' => 5,
+ 'backoff' => '1s',
+ 'headers' => {
+ 'Authorization' => ['<replace_with_a_secret_token>']
+ }
+ }
+ ]
+ ```
+
+ NOTE: **Note:**
+ If you use an external Registry (not the one integrated with GitLab), you must add
+ these settings to its configuration. In this case, you will also have to specify
+ notification secret in `registry.notification_secret` section of
+ `/etc/gitlab/gitlab.rb` file.
+
+1. Reconfigure the **primary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+### Configure **secondary** node
+
+Make sure you have Container Registry set up and working on
+the **secondary** node before following the next steps.
+
+The following steps should be done on each **secondary** node you're
+expecting to see the Docker images replicated.
+
+Because we need to allow the **secondary** node to communicate securely with
+the **primary** node Container Registry, we need to have a single key
+pair for all the nodes. The **secondary** node will use this key to
+generate a short-lived JWT that is pull-only-capable to access the
+**primary** node Container Registry.
+
+1. SSH into the **secondary** node and login as the `root` user:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the **secondary** node.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['registry_replication'] = {
+ enabled: true,
+ primary_api_url: 'http://primary.example.com:5000/' # internal address to the primary registry, will be used by GitLab to directly communicate with primary registry API
+ }
+ ```
+
+1. Reconfigure the **secondary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+### Verify replication
-[ee]: https://about.gitlab.com/pricing/
-[Docker Registry]: https://docs.docker.com/registry/
-[registry-storage]: ../../container_registry.md#container-registry-storage-driver
-[registry-load-balancing]: https://docs.docker.com/registry/deploying/#load-balancing-considerations
+To verify Container Registry replication is working, go to **Admin Area > Geo** (`/admin/geo/nodes`) on the **secondary** node.
+The initial replication, or "backfill", will probably still be in progress.
+You can monitor the synchronization process on each Geo node from the **primary** node's **Geo Nodes** dashboard in your browser.
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index dbd466b906d..1d50c924f6e 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -274,7 +274,7 @@ You can keep track of the progress to include the missing items in:
| [Server-side Git Hooks](../../custom_hooks.md) | No | No |
| [Elasticsearch integration](../../../integration/elasticsearch.md) | No | No |
| [GitLab Pages](../../pages/index.md) | No | No |
-| [Container Registry](../../container_registry.md) ([track progress](https://gitlab.com/gitlab-org/gitlab-ee/issues/2870)) | No | No |
+| [Container Registry](../../container_registry.md) | Yes | No |
| [NPM Registry](../../npm_registry.md) | No | No |
| [Maven Packages](../../maven_packages.md) | No | No |
| [External merge request diffs](../../merge_request_diffs.md) | No, if they are on-disk | No |
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 0c5611aa6cd..19c564c7616 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -64,7 +64,7 @@ Array.methods.grep(/sing/)
## Find method source
-Works for [non-instrumented methods](https://docs.gitlab.com/ce/development/instrumentation.html#checking-instrumented-methods):
+Works for [non-instrumented methods](../../development/instrumentation.md#checking-instrumented-methods):
```ruby
instance_of_object.method(:foo).source_location
@@ -474,7 +474,7 @@ User.active.count
```
```bash
-# Using curl and jq (up to a max 100, see [pagination](https://docs.gitlab.com/ee/api/#pagination)
+# Using curl and jq (up to a max 100, see pagination docs https://docs.gitlab.com/ee/api/#pagination
curl --silent --header "Private-Token: ********************" "https://gitlab.example.com/api/v4/users?per_page=100&active" | jq --compact-output '.[] | [.id,.name,.username]'
```
@@ -744,7 +744,7 @@ build.dependencies.each do |d| { puts "status: #{d.status}, finished at: #{d.fin
### Disable strict artifact checking (Introduced in GitLab 10.3.0)
-See <https://docs.gitlab.com/ee/administration/job_artifacts.html#validation-for-dependencies>.
+See [job artifacts documentation](../job_artifacts.md#validation-for-dependencies).
```ruby
Feature.enable('ci_disable_validates_dependencies')
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 6bcd4c48e5a..1247060058b 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -177,7 +177,7 @@ and they will assist you with any issues you are having.
```
After <https://gitlab.com/gitlab-org/charts/gitlab/issues/780> is fixed, it should
- be possible to use [Updating GitLab using the Helm Chart](https://docs.gitlab.com/ee/install/kubernetes/gitlab_chart.html#updating-gitlab-using-the-helm-chart)
+ be possible to use [Updating GitLab using the Helm Chart](https://docs.gitlab.com/charts/index.html#updating-gitlab-using-the-helm-chart)
for upgrades.
- How to apply changes to GitLab config:
@@ -244,7 +244,7 @@ to those documents for details.
on your workstation.
- When all the pods show either a `Running` or `Completed` status, get the GitLab password as
- described in [Initial login](https://docs.gitlab.com/ee/install/kubernetes/gitlab_chart.html#initial-login),
+ described in [Initial login](https://docs.gitlab.com/charts/installation/deployment.html#initial-login),
and log in to GitLab via the UI. It will be accessible via `https://gitlab.domain`
where `domain` is the value provided in the yaml file.
diff --git a/doc/administration/troubleshooting/linux_cheat_sheet.md b/doc/administration/troubleshooting/linux_cheat_sheet.md
index 2bbb498f020..853f553571c 100644
--- a/doc/administration/troubleshooting/linux_cheat_sheet.md
+++ b/doc/administration/troubleshooting/linux_cheat_sheet.md
@@ -259,7 +259,7 @@ then compare summaries of both results and dive into the differences.
Rough numbers for calls to `open` and `openat` (used to access files) on various configurations.
Slow storage can cause the dreaded `DeadlineExceeded` error in Gitaly.
-Also [see this entry](https://docs.gitlab.com/ee/administration/operations/filesystem_benchmarking.html)
+Also [see this entry](../operations/filesystem_benchmarking.md)
in the handbook for quick tests customers can perform to check their filesystem performance.
Keep in mind that timing information from `strace` is often somewhat inaccurate, so
diff --git a/doc/administration/troubleshooting/test_environments.md b/doc/administration/troubleshooting/test_environments.md
index 075effc5dc3..f1cdaf580a3 100644
--- a/doc/administration/troubleshooting/test_environments.md
+++ b/doc/administration/troubleshooting/test_environments.md
@@ -23,7 +23,7 @@ but contributions are welcome.
### GitLab
-Please see [our Docker test environment docs](https://docs.gitlab.com/ee/install/digitaloceandocker.html#create-new-gitlab-container)
+Please see [our Docker test environment docs](../../install/digitaloceandocker.md#create-new-gitlab-container)
for how to run GitLab on Docker. When spinning this up with `docker-machine`, ensure
you change a few things:
@@ -59,7 +59,7 @@ docker run --name gitlab_saml -p 8080:8080 -p 8443:8443 \
-d jamedjo/test-saml-idp
```
-The following will also need to go in your `/etc/gitlab/gitlab.rb`. See [our SAML docs](https://docs.gitlab.com/ee/integration/saml.html)
+The following will also need to go in your `/etc/gitlab/gitlab.rb`. See [our SAML docs](../../integration/saml.md)
for more, as well as the list of [default usernames, passwords, and emails](https://hub.docker.com/r/jamedjo/test-saml-idp/#usage).
```ruby
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 0d030ef30c8..c7637ad23de 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -821,6 +821,66 @@ Parameters:
]
```
+## Create MR Pipeline
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31722) in Gitlab 12.3.
+
+Create a new [pipeline for a merge request](../ci/merge_request_pipelines/index.md). A pipeline created via this endpoint will not run a regular branch/tag pipeline, it requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs.
+
+The new pipeline can be:
+
+- A detached merge request pipeline.
+- A [pipeline for merged results](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md)
+ if the [project setting is enabled](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md#enabling-pipelines-for-merged-results).
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/pipelines
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
+- `merge_request_iid` (required) - The internal ID of the merge request
+
+```json
+{
+ "id": 2,
+ "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "ref": "refs/merge-requests/1/head",
+ "status": "pending",
+ "web_url": "http://localhost/user1/project1/pipelines/2",
+ "before_sha": "0000000000000000000000000000000000000000",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "id": 1,
+ "name": "John Doe1",
+ "username": "user1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://example.com"
+ },
+ "created_at": "2019-09-04T19:20:18.267Z",
+ "updated_at": "2019-09-04T19:20:18.459Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null,
+ "coverage": null,
+ "detailed_status": {
+ "icon": "status_pending",
+ "text": "pending",
+ "label": "pending",
+ "group": "pending",
+ "tooltip": "pending",
+ "has_details": false,
+ "details_path": "/user1/project1/pipelines/2",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png"
+ }
+}
+```
+
## Create MR
Creates a new merge request.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index fc0125fcc18..4da527154ad 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -580,7 +580,7 @@ For private and internal projects:
If you want to use your own Docker images for docker-in-docker there are a few things you need to do in addition to the steps in the [docker-in-docker](#use-docker-in-docker-workflow-with-docker-executor) section:
1. Update the `image` and `service` to point to your registry.
-1. Add a service [alias](https://docs.gitlab.com/ee/ci/yaml/#servicesalias).
+1. Add a service [alias](../yaml/README.md#servicesalias).
Below is an example of what your `.gitlab-ci.yml` should look like,
assuming you have it configured with [TLS enabled](#tls-enabled):
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
index 9c65de115b4..88bcead7beb 100644
--- a/doc/ci/examples/code_quality.md
+++ b/doc/ci/examples/code_quality.md
@@ -1,118 +1,5 @@
---
-disqus_identifier: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html'
-type: reference, howto
+redirect_to: '../../user/project/merge_requests/code_quality.md#example-configuration'
---
-# Analyze your project's Code Quality
-
-CAUTION: **Caution:**
-The job definition shown below is supported on GitLab 11.11 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-This example shows how to run Code Quality on your code by using GitLab CI/CD
-and Docker.
-
-First, you need GitLab Runner with
-[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
-
-Once you set up the Runner, include the CodeQuality template in your CI config:
-
-```yaml
-include:
- - template: Code-Quality.gitlab-ci.yml
-```
-
-The above example will create a `code_quality` job in your CI/CD pipeline which
-will scan your source code for code quality issues. The report will be saved as a
-[Code Quality report artifact](../yaml/README.md#artifactsreportscodequality-starter)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest Code Quality artifact available.
-
-TIP: **Tip:**
-For [GitLab Starter][ee] users, this information will be automatically
-extracted and shown right in the merge request widget.
-[Learn more on Code Quality in merge requests](../../user/project/merge_requests/code_quality.md).
-
-CAUTION: **Caution:**
-On self-managed instances, if a malicious actor compromises the Code Quality job
-definition they will be able to execute privileged docker commands on the Runner
-host. Having proper access control policies mitigates this attack vector by
-allowing access only to trusted actors.
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, Code Quality job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.5 and earlier, the job should look like:
-
-```yaml
-code_quality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- reports:
- codequality: gl-code-quality-report.json
-```
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-code_quality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- paths: [gl-code-quality-report.json]
-```
-
-Alternatively the job name could be `codeclimate` or `codequality`
-and the artifact name could be `codeclimate.json`.
-These names have been deprecated with GitLab 11.0
-and may be removed in next major release, GitLab 12.0.
-
-For GitLab 10.3 and earlier, the job should look like:
-
-```yaml
-codequality:
- image: docker:latest
- variables:
- DOCKER_DRIVER: overlay
- services:
- - docker:dind
- script:
- - docker pull codeclimate/codeclimate:0.69.0
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
- artifacts:
- paths: [codeclimate.json]
-```
-
-[cli]: https://github.com/codeclimate/codeclimate
-[ee]: https://about.gitlab.com/pricing/
+This document was moved to [another location](../../user/project/merge_requests/code_quality.md#example-configuration).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 8aae0e85c89..61012d8653a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2089,7 +2089,7 @@ staging:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23464) in GitLab 12.3.
`interruptible` is used to indicate that a job should be canceled if made redundant by a newer run of the same job. Defaults to `true`.
-This value will only be used if the [automatic cancellation of redundant pipelines feature](https://docs.gitlab.com/ee/user/project/pipelines/settings.html#auto-cancel-pending-pipelines)
+This value will only be used if the [automatic cancellation of redundant pipelines feature](../../user/project/pipelines/settings.md#auto-cancel-pending-pipelines)
is enabled.
When enabled, a pipeline on the same branch will be canceled when:
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index 13c406727ab..576aad17c0a 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -115,7 +115,7 @@ To subscribe to GitLab through a self-managed installation:
1. [Install](https://about.gitlab.com/install/) GitLab.
1. Complete the installation with
- [administration tasks](https://docs.gitlab.com/ee/administration/).
+ [administration tasks](../administration/index.md).
1. Select the **Starter**, **Premium**, or **Ultimate** self-managed plan
through the [GitLab Subscription Manager](https://customers.gitlab.com/).
1. Apply your license file. After purchase, a license file is sent to the email
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 15fdb52ac00..0cbd85fc568 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -780,15 +780,19 @@ It is also possible to copy and paste the contents of the [Auto DevOps
template] into your project and edit this as needed. You may prefer to do it
that way if you want to specifically remove any part of it.
-### Using components of Auto-DevOps
+### Using components of Auto DevOps
-If you only require a subset of the features offered by Auto-DevOps, you can include
-individual Auto-DevOps jobs into your own `.gitlab-ci.yml`.
+If you only require a subset of the features offered by Auto DevOps, you can include
+individual Auto DevOps jobs into your own `.gitlab-ci.yml`. Each component job relies
+on a stage that should be defined in the `.gitlab-ci.yml` that includes the template.
For example, to make use of [Auto Build](#auto-build), you can add the following to
your `.gitlab-ci.yml`:
```yaml
+stages:
+ - build
+
include:
- template: Jobs/Build.gitlab-ci.yml
```
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 6b748981106..a198d83fac5 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -36,6 +36,9 @@ Thread resolution helps keep track of progress during planning or code review.
Every standard comment or thread in merge requests, commits, commit diffs, and
snippets is initially displayed as unresolved. They can then be individually resolved by anyone
with at least Developer access to the project or by the author of the change being reviewed.
+If the thread has been resolved and a non-member unresolves their own response,
+this will also unresolve the discussion thread.
+If the non-member then resolves this same response, this will resolve the discussion thread.
The need to resolve all standard comments or threads prevents you from forgetting
to address feedback and lets you hide threads that are no longer relevant.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 75a1ac8e9ff..d3cbe4d545f 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -997,16 +997,18 @@ These details <em>will</em> remain <strong>hidden</strong> until expanded.
Markdown inside these tags is supported as well, as long as you have a blank line
after the `</summary>` tag and before the `</details>` tag, as shown in the example:
-```html
+````html
<details>
<summary>Click me to collapse/fold.</summary>
These details _will_ remain **hidden** until expanded.
+```
PASTE LOGS HERE
+```
</details>
-```
+````
<!-- Note: The example below uses HTML to force correct rendering on docs.gitlab.com, markdown will be fine in GitLab -->
@@ -1015,7 +1017,7 @@ PASTE LOGS HERE
These details <em>will</em> remain <b>hidden</b> until expanded.
-PASTE LOGS HERE
+<pre><code>PASTE LOGS HERE</code></pre>
</details>
@@ -1161,16 +1163,15 @@ GFM will autolink almost any URL you put into your text:
### Lists
-Ordered and unordered lists can be easily created. Add the number you want the list
+Ordered and unordered lists can be easily created.
+
+For an ordered list, add the number you want the list
to start with, like `1.`, followed by a space, at the start of each line for ordered lists.
After the first number, it does not matter what number you use, ordered lists will be
numbered automatically by vertical order, so repeating `1.` for all items in the
same list is common. If you start with a number other than `1.`, it will use that as the first
number, and count up from there.
-Add a `*`, `-` or `+`, followed by a space, at the start of each line for unordered lists, but
-you should not use a mix of them.
-
Examples:
```md
@@ -1181,15 +1182,10 @@ Examples:
1. Ordered sub-list
1. Next ordered sub-list item
4. And another item.
-
-* Unordered lists can use asterisks
-
-- Or minuses
-
-+ Or pluses
```
-<!-- The "2." and "4." in the example above are changed to "1." below, only to match the standards on docs.gitlab.com -->
+<!-- The "2." and "4." in the example above are changed to "1." below, to match the style standards on docs.gitlab.com -->
+<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
1. First ordered list item
1. Another item
@@ -1199,11 +1195,43 @@ Examples:
1. Next ordered sub-list item
1. And another item.
-- Unordered lists can use asterisks
+For an unordered list, add a `-`, `*` or `+`, followed by a space, at the start of
+each line for unordered lists, but you should not use a mix of them.
+
+```md
+Unordered lists can:
+
+- use
+- minuses
+
+They can also:
+
+* use
+* asterisks
+
+They can even:
+
++ use
++ pluses
+```
+
+<!-- The "*" and "+" in the example above are changed to "-" below, to match the style standards on docs.gitlab.com -->
+<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
+
+Unordered lists can:
+
+- use
+- minuses
+
+They can also:
+
+- use
+- asterisks
-- Or minuses
+They can even:
-- Or pluses
+- use
+- pluses
---
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 3c6b660c63d..e7ecd173597 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -1,6 +1,5 @@
---
type: reference, howto
-disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html'
---
# Code Quality **(STARTER)**
@@ -18,7 +17,7 @@ Code Quality:
- Runs in [pipelines](../../../ci/pipelines.md) using an Docker image built in
[GitLab Code
Quality](https://gitlab.com/gitlab-org/security-products/codequality) project.
-- Can make use of a [template](#template-and-examples).
+- Can make use of a [template](#example-configuration).
- Is available with [Auto
DevOps](../../../topics/autodevops/index.md#auto-code-quality-starter).
@@ -42,14 +41,112 @@ For instance, consider the following workflow:
1. You approve the merge request and authorize its deployment to staging.
1. Once verified, their changes are deployed to production.
-## Template and examples
+## Example configuration
-For most GitLab instances, the supplied template is the preferred method of
-implementing Code Quality. See
-[Analyze your project's Code Quality](../../../ci/examples/code_quality.md) for:
+CAUTION: **Caution:**
+The job definition shown below is supported on GitLab 11.11 and later versions. It
+also requires the GitLab Runner 11.5 or later. For earlier versions, use the
+[previous job definitions](#previous-job-definitions).
-- Information on the builtin GitLab Code Quality template.
-- Examples of manual GitLab configuration for earlier GitLab versions.
+This example shows how to run Code Quality on your code by using GitLab CI/CD and Docker.
+
+First, you need GitLab Runner with
+[docker-in-docker executor](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+
+Once you set up the Runner, include the CodeQuality template in your CI config:
+
+```yaml
+include:
+ - template: Code-Quality.gitlab-ci.yml
+```
+
+The above example will create a `code_quality` job in your CI/CD pipeline which
+will scan your source code for code quality issues. The report will be saved as a
+[Code Quality report artifact](../../../ci/yaml/README.md#artifactsreportscodequality-starter)
+that you can later download and analyze. Due to implementation limitations we always
+take the latest Code Quality artifact available.
+
+TIP: **Tip:**
+This information will be automatically extracted and shown right in the merge request widget.
+
+CAUTION: **Caution:**
+On self-managed instances, if a malicious actor compromises the Code Quality job
+definition they will be able to execute privileged docker commands on the Runner
+host. Having proper access control policies mitigates this attack vector by
+allowing access only to trusted actors.
+
+### Previous job definitions
+
+CAUTION: **Caution:**
+Before GitLab 11.5, Code Quality job and artifact had to be named specifically to
+automatically extract report data and show it in the merge request widget. While these
+old job definitions are still maintained they have been deprecated and may be removed
+in the next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change.
+
+For GitLab 11.5 and earlier, the job should look like:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ reports:
+ codequality: gl-code-quality-report.json
+```
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ paths: [gl-code-quality-report.json]
+```
+
+Alternatively the job name could be `codeclimate` or `codequality` and the artifact
+name could be `codeclimate.json`. These names have been deprecated with GitLab 11.0
+and may be removed in the next major release, GitLab 12.0.
+
+For GitLab 10.3 and earlier, the job should look like:
+
+```yaml
+codequality:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay
+ services:
+ - docker:dind
+ script:
+ - docker pull codeclimate/codeclimate:0.69.0
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
+ artifacts:
+ paths: [codeclimate.json]
+```
## Configuring jobs using variables
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 64ee82cd775..4c092f10729 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -317,6 +317,26 @@ module API
present paginate(pipelines), with: Entities::PipelineBasic
end
+ desc 'Create a pipeline for merge request' do
+ success Entities::Pipeline
+ end
+ post ':id/merge_requests/:merge_request_iid/pipelines' do
+ authorize! :create_pipeline, user_project
+
+ pipeline = ::MergeRequests::CreatePipelineService
+ .new(user_project, current_user, allow_duplicate: true)
+ .execute(find_merge_request_with_access(params[:merge_request_iid]))
+
+ if pipeline.nil?
+ not_allowed!
+ elsif pipeline.persisted?
+ status :ok
+ present pipeline, with: Entities::Pipeline
+ else
+ render_validation_error!(pipeline)
+ end
+ end
+
desc 'Update a merge request' do
success Entities::MergeRequest
end
diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb
index be73bcd5506..6f760751b0f 100644
--- a/lib/gitlab/issuable_metadata.rb
+++ b/lib/gitlab/issuable_metadata.rb
@@ -19,7 +19,7 @@ module Gitlab
return {} if issuable_ids.empty?
- issuable_note_count = ::Note.count_for_collection(issuable_ids, collection_type)
+ issuable_notes_count = ::Note.count_for_collection(issuable_ids, collection_type)
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
issuable_merge_requests_count =
if collection_type == 'Issue'
@@ -31,7 +31,7 @@ module Gitlab
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
- notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ notes = issuable_notes_count.find { |notes| notes.noteable_id == id }
merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
issuable_meta[id] = ::Issuable::IssuableMeta.new(
diff --git a/lib/gitlab/noteable_metadata.rb b/lib/gitlab/noteable_metadata.rb
new file mode 100644
index 00000000000..f3f8933b81f
--- /dev/null
+++ b/lib/gitlab/noteable_metadata.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module NoteableMetadata
+ def noteable_meta_data(noteable_collection, collection_type)
+ # ActiveRecord uses Object#extend for null relations.
+ if !(noteable_collection.singleton_class < ActiveRecord::NullRelation) &&
+ noteable_collection.respond_to?(:limit_value) &&
+ noteable_collection.limit_value.nil?
+
+ raise 'Collection must have a limit applied for preloading meta-data'
+ end
+
+ # map has to be used here since using pluck or select will
+ # throw an error when ordering noteables which inserts
+ # a new order into the collection.
+ # We cannot use reorder to not mess up the paginated collection.
+ noteable_ids = noteable_collection.map(&:id)
+
+ return {} if noteable_ids.empty?
+
+ noteable_notes_count = ::Note.count_for_collection(noteable_ids, collection_type)
+
+ noteable_ids.each_with_object({}) do |id, noteable_meta|
+ notes = noteable_notes_count.find { |notes| notes.noteable_id == id }
+
+ noteable_meta[id] = ::Noteable::NoteableMeta.new(
+ notes.try(:count).to_i
+ )
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 32deab7dd68..f2d3a39d593 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1146,6 +1146,9 @@ msgstr ""
msgid "An error occurred while triggering the job."
msgstr ""
+msgid "An error occurred while trying to run a new pipeline for this Merge Request."
+msgstr ""
+
msgid "An error occurred while validating username"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 8b38011486b..9bf28d396ba 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -66,6 +66,7 @@ module QA
autoload :Fork, 'qa/resource/fork'
autoload :SSHKey, 'qa/resource/ssh_key'
autoload :Snippet, 'qa/resource/snippet'
+ autoload :ProjectMember, 'qa/resource/project_member'
module Events
autoload :Base, 'qa/resource/events/base'
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 84353e3d0c7..24b9fc67dd9 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -85,6 +85,8 @@ module QA
end
def add_file(name, contents)
+ FileUtils.mkdir_p(::File.dirname(name))
+
::File.write(name, contents)
if use_lfs?
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 94245bbfcba..65d83926f38 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -148,6 +148,12 @@ module QA
click_element :saml_login_button
end
+ def sign_out_and_sign_in_as(user:)
+ Menu.perform(&:sign_out)
+ has_sign_in_tab?
+ sign_in_using_credentials(user)
+ end
+
private
def sign_in_using_gitlab_credentials(user)
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 72f8e1c3ef0..6e550805f9f 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -119,7 +119,7 @@ module QA
has_element?(:description, text: description)
end
- def merge!
+ def try_to_merge!
# The merge button is disabled on load
wait do
has_element?(:merge_button)
@@ -131,6 +131,10 @@ module QA
end
merge_immediately
+ end
+
+ def merge!
+ try_to_merge!
success = wait do
has_text?('The changes were merged into')
diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb
index c53d805c61d..65149e631f3 100644
--- a/qa/qa/page/project/sub_menus/repository.rb
+++ b/qa/qa/page/project/sub_menus/repository.rb
@@ -44,3 +44,5 @@ module QA
end
end
end
+
+QA::Page::Project::SubMenus::Repository.prepend_if_ee('QA::EE::Page::Project::SubMenus::Repository')
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index d1d75b6179e..e4f708dc251 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -11,6 +11,7 @@ module QA
ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
+ ResourceNotDeletedError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
attr_writer :api_client
@@ -30,6 +31,10 @@ module QA
resource_web_url(api_post)
end
+ def remove_via_api!
+ api_delete
+ end
+
def eager_load_api_client!
return unless api_client.nil?
@@ -79,6 +84,17 @@ module QA
process_api_response(parse_body(response))
end
+ def api_delete
+ url = Runtime::API::Request.new(api_client, api_delete_path).url
+ response = delete(url)
+
+ unless response.code == HTTP_STATUS_NO_CONTENT
+ raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
+ end
+
+ response
+ end
+
def api_client
@api_client ||= begin
Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'), user: user)
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 283fc6cdbcb..88069df6ade 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -47,6 +47,18 @@ module QA
end
end
+ def self.remove_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.remove_via_api! }
+ end
+ end
+
def fabricate!(*_args)
raise NotImplementedError
end
diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb
index a45dd030625..6dc47e36977 100644
--- a/qa/qa/resource/branch.rb
+++ b/qa/qa/resource/branch.rb
@@ -72,6 +72,18 @@ module QA
end
end
end
+
+ def self.unprotect_via_api!(&block)
+ self.remove_via_api!(&block)
+ end
+
+ def api_get_path
+ "/projects/#{@project.api_resource[:id]}/protected_branches/#{@branch_name}"
+ end
+
+ def api_delete_path
+ "/projects/#{@project.api_resource[:id]}/protected_branches/#{@branch_name}"
+ end
end
end
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 53126c67ba3..fe7eeeed37a 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -17,6 +17,7 @@ module QA
:labels,
:file_name,
:file_content
+ attr_writer :no_preparation
attribute :project do
Project.fabricate! do |resource|
@@ -58,6 +59,7 @@ module QA
@file_name = "added_file.txt"
@file_content = "File Added"
@target_new_branch = true
+ @no_preparation = false
end
def fabricate!
@@ -80,7 +82,7 @@ module QA
end
def fabricate_via_api!
- populate(:target, :source)
+ populate(:target, :source) unless @no_preparation
super
end
diff --git a/qa/qa/resource/project_member.rb b/qa/qa/resource/project_member.rb
new file mode 100644
index 00000000000..dfaa157038c
--- /dev/null
+++ b/qa/qa/resource/project_member.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class ProjectMember < Base
+ attr_accessor :user, :project, :access_level
+ attr_reader :level
+
+ def initialize
+ @level = {
+ guest: 10,
+ reporter: 20,
+ developer: 30,
+ maintainer: 40,
+ owner: 50
+ }
+ end
+
+ def api_get_path
+ "/projects/#{project.api_resource[:id]}/members/#{user.api_resource[:id]}"
+ end
+
+ def api_post_path
+ "/projects/#{project.api_resource[:id]}/members"
+ end
+
+ def api_post_body
+ {
+ user_id: user.api_resource[:id],
+ access_level: access_level
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index 203064b2665..d0ff1f8bc2c 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -5,6 +5,7 @@ module QA
module Api
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
+ HTTP_STATUS_NO_CONTENT = 204
def post(url, payload)
RestClient::Request.execute(
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
index 9c9f658a0bd..2f3c7da484b 100644
--- a/spec/controllers/admin/applications_controller_spec.rb
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -10,6 +10,16 @@ describe Admin::ApplicationsController do
sign_in(admin)
end
+ describe 'GET #index' do
+ render_views
+
+ it 'renders the application form' do
+ get :index
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
describe 'GET #new' do
it 'renders the application form' do
get :new
diff --git a/spec/controllers/dashboard/snippets_controller_spec.rb b/spec/controllers/dashboard/snippets_controller_spec.rb
new file mode 100644
index 00000000000..2d839094d34
--- /dev/null
+++ b/spec/controllers/dashboard/snippets_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Dashboard::SnippetsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+
+ before do
+ create(:personal_snippet, :public, author: user)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 9a3fbfaac51..c5af04f72ee 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -82,35 +82,15 @@ describe Dashboard::TodosController do
end
end
- context 'when using pagination' do
- let(:last_page) { user.todos.page.total_pages }
+ it_behaves_like 'paginated collection' do
let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
+ let(:collection) { user.todos }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index, params: { page: (last_page + 1).to_param }
-
- expect(response).to redirect_to(dashboard_todos_path(page: last_page))
- end
-
- it 'goes to the correct page' do
- get :index, params: { page: last_page }
-
- expect(assigns(:todos).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not redirect to external sites when provided a host field' do
- external_host = "www.example.com"
- get :index, params: { page: (last_page + 1).to_param, host: external_host }
-
- expect(response).to redirect_to(dashboard_todos_path(page: last_page))
- end
-
context 'when providing no filters' do
it 'does not perform a query to get the page count, but gets that from the user' do
allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/explore/snippets_controller_spec.rb b/spec/controllers/explore/snippets_controller_spec.rb
new file mode 100644
index 00000000000..fa659c6df7f
--- /dev/null
+++ b/spec/controllers/explore/snippets_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Explore::SnippetsController do
+ describe 'GET #index' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+
+ before do
+ create(:personal_snippet, :public)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 5ac5279e997..80b5eb9a7ee 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
- let(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:forked_project) { Projects::ForkService.new(project, user, name: 'Some name').execute }
let(:group) { create(:group) }
before do
@@ -13,11 +13,12 @@ describe Projects::ForksController do
end
describe 'GET index' do
- def get_forks
+ def get_forks(search: nil)
get :index,
params: {
namespace_id: project.namespace,
- project_id: project
+ project_id: project,
+ search: search
}
end
@@ -31,6 +32,41 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(1)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
+
+ context 'after search' do
+ it 'forks counts are correct' do
+ get_forks(search: 'Non-matching query')
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(1)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
+ end
+ end
+
+ context 'when fork is internal' do
+ before do
+ forked_project.update(visibility_level: Project::INTERNAL, group: group)
+ end
+
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(0)
+ expect(assigns[:internal_forks_count]).to eq(1)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
end
context 'when fork is private' do
@@ -38,12 +74,25 @@ describe Projects::ForksController do
forked_project.update(visibility_level: Project::PRIVATE, group: group)
end
- it 'is not be visible for non logged in users' do
+ shared_examples 'forks counts' do
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(0)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(1)
+ end
+ end
+
+ it 'is not visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_blank
end
+ include_examples 'forks counts'
+
context 'when user is logged in' do
before do
sign_in(project.creator)
@@ -67,6 +116,8 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ include_examples 'forks counts'
end
context 'when user is a member of the Group' do
@@ -79,6 +130,8 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ include_examples 'forks counts'
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 608131dcbc8..367bd641f5d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -71,9 +71,16 @@ describe Projects::IssuesController do
end
end
- context 'with page param' do
- let(:last_page) { project.issues.page.total_pages }
+ it_behaves_like 'paginated collection' do
let!(:issue_list) { create_list(:issue, 2, project: project) }
+ let(:collection) { project.issues }
+ let(:params) do
+ {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ state: 'opened'
+ }
+ end
before do
sign_in(user)
@@ -81,51 +88,10 @@ describe Projects::IssuesController do
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param
- }
-
- expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
- end
-
- it 'redirects to specified page' do
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: last_page.to_param
- }
-
- expect(assigns(:issues).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not redirect to external sites when provided a host field' do
- external_host = "www.example.com"
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param,
- host: external_host
- }
-
- expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
- end
-
it 'does not use pagination if disabled' do
allow(controller).to receive(:pagination_disabled?).and_return(true)
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param
- }
+ get :index, params: params.merge(page: last_page + 1)
expect(response).to have_gitlab_http_status(200)
expect(assigns(:issues).size).to eq(2)
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 9b5d7317c11..b13534b9088 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -13,31 +13,17 @@ describe Projects::SnippetsController do
end
describe 'GET #index' do
- context 'when page param' do
- let(:last_page) { project.snippets.page.total_pages }
- let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) }
-
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- page: (last_page + 1).to_param
- }
-
- expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
+ it_behaves_like 'paginated collection' do
+ let(:collection) { project.snippets }
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
end
- it 'redirects to specified page' do
- get :index,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- page: last_page.to_param
- }
-
- expect(assigns(:snippets).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
+ before do
+ create(:project_snippet, :public, project: project, author: user)
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index b0092bc8994..1b3a8965342 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -9,6 +9,15 @@ describe SnippetsController do
let(:user) { create(:user) }
context 'when username parameter is present' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+ let(:params) { { username: user.username } }
+
+ before do
+ create(:personal_snippet, :public, author: user)
+ end
+ end
+
it 'renders snippets of a user when username is present' do
get :index, params: { username: user.username }
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index f04317a59ee..7a8b938486a 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -45,6 +45,38 @@ describe 'Merge request > User sees pipelines', :js do
expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
end
+
+ context 'with a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+
+ it 'displays the Run Pipeline button' do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+
+ wait_for_requests
+
+ expect(page.find('.js-run-mr-pipeline')).to have_text('Run Pipeline')
+ end
+ end
+
+ context 'with a merged results pipeline' do
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+
+ it 'displays the Run Pipeline button' do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+
+ wait_for_requests
+
+ expect(page.find('.js-run-mr-pipeline')).to have_text('Run Pipeline')
+ end
+ end
end
context 'without pipelines' do
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 2aed402652b..6792a6e2af0 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -121,7 +121,6 @@ describe 'Project fork' do
end
expect(page).not_to have_content("#{another_project_fork.namespace.human_name} / #{another_project_fork.name}")
- expect(page).to have_content("1 private fork")
end
end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index fec01b1f0a3..46aca2b7f03 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import Api from '~/api';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -10,6 +11,13 @@ describe('Pipelines table in Commits and Merge requests', function() {
let PipelinesTable;
let mock;
let vm;
+ const props = {
+ endpoint: 'endpoint.json',
+ helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
+ autoDevopsHelpPath: 'foo',
+ };
preloadFixtures(jsonFixtureName);
@@ -32,13 +40,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
beforeEach(function() {
mock.onGet('endpoint.json').reply(200, []);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render the empty state', function(done) {
@@ -54,13 +56,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
describe('with pipelines', () => {
beforeEach(() => {
mock.onGet('endpoint.json').reply(200, [pipeline]);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render a table with the received pipelines', done => {
@@ -111,30 +107,145 @@ describe('Pipelines table in Commits and Merge requests', function() {
done();
});
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
element.appendChild(vm.$el);
});
});
});
+ describe('run pipeline button', () => {
+ let pipelineCopy;
+
+ beforeEach(() => {
+ pipelineCopy = Object.assign({}, pipeline);
+ });
+
+ describe('when latest pipeline has detached flag and canRunPipeline is true', () => {
+ it('renders the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline has detached flag and canRunPipeline is false', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: false,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline does not have detached flag and canRunPipeline is true', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = false;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline does not have detached flag and merge_request_pipeline is true', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: false,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('on click', () => {
+ beforeEach(() => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ }),
+ );
+ });
+
+ it('updates the loading state', done => {
+ spyOn(Api, 'postMergeRequestPipeline').and.returnValue(Promise.resolve());
+
+ setTimeout(() => {
+ vm.$el.querySelector('.js-run-mr-pipeline').click();
+
+ vm.$nextTick(() => {
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(true);
+
+ setTimeout(() => {
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(false);
+
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+
describe('unsuccessfull request', () => {
beforeEach(() => {
mock.onGet('endpoint.json').reply(500, []);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render error state', function(done) {
diff --git a/spec/lib/gitlab/noteable_metadata_spec.rb b/spec/lib/gitlab/noteable_metadata_spec.rb
new file mode 100644
index 00000000000..b12a1825f04
--- /dev/null
+++ b/spec/lib/gitlab/noteable_metadata_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::NoteableMetadata do
+ subject { Class.new { include Gitlab::NoteableMetadata }.new }
+
+ it 'returns an empty Hash if an empty collection is provided' do
+ expect(subject.noteable_meta_data(Snippet.none, 'Snippet')).to eq({})
+ end
+
+ it 'raises an error when given a collection with no limit' do
+ expect { subject.noteable_meta_data(Snippet.all, 'Snippet') }.to raise_error(/must have a limit/)
+ end
+
+ context 'snippets' do
+ let!(:snippet) { create(:personal_snippet) }
+ let!(:other_snippet) { create(:personal_snippet) }
+ let!(:note) { create(:note, noteable: snippet) }
+
+ it 'aggregates stats on snippets' do
+ data = subject.noteable_meta_data(Snippet.all.limit(10), 'Snippet')
+
+ expect(data.count).to eq(2)
+ expect(data[snippet.id].user_notes_count).to eq(1)
+ expect(data[other_snippet.id].user_notes_count).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 146e479adef..d5ad70194cb 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -579,14 +579,22 @@ describe Ci::Pipeline, :mailer do
end
describe 'Validations for merge request pipelines' do
- let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) }
+ let(:pipeline) do
+ build(:ci_pipeline, source: source, merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master')
+ end
context 'when source is merge request' do
let(:source) { :merge_request_event }
context 'when merge request is specified' do
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
-
it { expect(pipeline).to be_valid }
end
@@ -601,8 +609,6 @@ describe Ci::Pipeline, :mailer do
let(:source) { :web }
context 'when merge request is specified' do
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
-
it { expect(pipeline).not_to be_valid }
end
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
new file mode 100644
index 00000000000..0a1c576a5e7
--- /dev/null
+++ b/spec/models/oauth_access_token_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe OauthAccessToken do
+ let(:user) { create(:user) }
+ let(:app_one) { create(:oauth_application) }
+ let(:app_two) { create(:oauth_application) }
+ let(:app_three) { create(:oauth_application) }
+ let(:tokens) { described_class.all }
+
+ before do
+ create(:oauth_access_token, application_id: app_one.id)
+ create_list(:oauth_access_token, 2, resource_owner: user, application_id: app_two.id)
+ end
+
+ it 'returns unique owners' do
+ expect(tokens.count).to eq(3)
+ expect(tokens.distinct_resource_owner_counts([app_one])).to eq({ app_one.id => 1 })
+ expect(tokens.distinct_resource_owner_counts([app_two])).to eq({ app_two.id => 1 })
+ expect(tokens.distinct_resource_owner_counts([app_three])).to eq({})
+ expect(tokens.distinct_resource_owner_counts([app_one, app_two]))
+ .to eq({
+ app_one.id => 1,
+ app_two.id => 1
+ })
+ end
+end
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index e47166544d9..53fc3096751 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Applications, :api do
- include ApiHelpers
-
let(:admin_user) { create(:user, admin: true) }
let(:user) { create(:user, admin: false) }
let!(:application) { create(:application, name: 'another_application', redirect_uri: 'http://other_application.url', scopes: '') }
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 018691e8099..2fc772b12af 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Events do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index aceff9b4aa6..68df02d4d8d 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::ImportGithub do
- include ApiHelpers
-
let(:token) { "asdasd12345" }
let(:provider) { :github }
let(:access_params) { { github_access_token: token } }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 15d6db42760..8179da2f97c 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1033,6 +1033,70 @@ describe API::MergeRequests do
end
end
+ describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:ci_yaml_file)
+ .and_return(YAML.dump({
+ rspec: {
+ script: 'ls',
+ only: ['merge_requests']
+ }
+ }))
+ end
+
+ let(:project) do
+ create(:project, :private, :repository,
+ creator: user,
+ namespace: user.namespace,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, :with_detached_merge_request_pipeline,
+ milestone: milestone1,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ title: 'Test',
+ created_at: base_time)
+ end
+
+ let(:merge_request_iid) { merge_request.iid }
+ let(:authenticated_user) { user }
+
+ let(:request) do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request_iid}/pipelines", authenticated_user)
+ end
+
+ context 'when authorized' do
+ it 'creates and returns the new Pipeline' do
+ expect { request }.to change(Ci::Pipeline, :count).by(1)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_a Hash
+ end
+ end
+
+ context 'when unauthorized' do
+ let(:authenticated_user) { create(:user) }
+
+ it 'responds with a blank 404' do
+ expect { request }.not_to change(Ci::Pipeline, :count)
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when the merge request does not exist' do
+ let(:merge_request_iid) { 777 }
+
+ it 'responds with a blank 404' do
+ expect { request }.not_to change(Ci::Pipeline, :count)
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
describe 'POST /projects/:id/merge_requests' do
context 'support for deprecated assignee_id' do
let(:params) do
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index 43df9993eb9..8c2db6e4c62 100644
--- a/spec/requests/api/project_events_spec.rb
+++ b/spec/requests/api/project_events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::ProjectEvents do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index 9479439bde8..576e8498e4d 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -38,6 +38,10 @@ describe MergeRequests::CreatePipelineService do
expect(subject).to be_detached_merge_request_pipeline
end
+ it 'defaults to merge_request_event' do
+ expect(subject.source).to eq('merge_request_event')
+ end
+
context 'when service is called multiple times' do
it 'creates a pipeline once' do
expect do
diff --git a/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb b/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb
new file mode 100644
index 00000000000..bd84bd1093f
--- /dev/null
+++ b/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples 'paginated collection' do
+ let(:collection) { nil }
+ let(:last_page) { collection.page.total_pages }
+ let(:action) { :index }
+ let(:params) { {} }
+
+ it 'renders a page number that is not ouf of range' do
+ get action, params: params.merge(page: last_page)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get action, params: params.merge(page: last_page + 1)
+
+ expect(response).to redirect_to(params.merge(page: last_page))
+ end
+
+ it 'does not redirect to external sites when provided a host field' do
+ external_host = 'www.example.com'
+
+ get action, params: params.merge(page: last_page + 1, host: external_host)
+
+ expect(response).to redirect_to(params.merge(page: last_page))
+ end
+end