Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml35
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js4
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue2
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js6
-rw-r--r--app/assets/javascripts/monitoring/constants.js2
-rw-r--r--app/assets/javascripts/monitoring/utils.js21
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue9
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue22
-rw-r--r--app/assets/javascripts/notes/mixins/draft.js8
-rw-r--r--app/assets/javascripts/notes/mixins/get_discussion.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/stylesheets/application.scss3
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss91
-rw-r--r--app/assets/stylesheets/pages/environments.scss338
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss270
-rw-r--r--app/assets/stylesheets/vendors/atwho.scss92
-rw-r--r--app/controllers/abuse_reports_controller.rb6
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/help_controller.rb33
-rw-r--r--app/controllers/invites_controller.rb8
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb12
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/profiles/chat_names_controller.rb10
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb6
-rw-r--r--app/controllers/profiles/preferences_controller.rb6
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/tags/releases_controller.rb10
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/graphql/gitlab_schema.rb9
-rw-r--r--app/models/concerns/noteable.rb8
-rw-r--r--app/models/gpg_signature.rb9
-rw-r--r--app/models/instance_configuration.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/release.rb1
-rw-r--r--app/services/after_branch_delete_service.rb20
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/delete_branch_service.rb10
-rw-r--r--app/services/git/base_hooks_service.rb94
-rw-r--r--app/services/git/branch_hooks_service.rb144
-rw-r--r--app/services/git/branch_push_service.rb215
-rw-r--r--app/services/git/tag_hooks_service.rb36
-rw-r--r--app/services/git/tag_push_service.rb50
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb162
-rw-r--r--app/services/merge_requests/refresh_service.rb10
-rw-r--r--app/services/note_summary.rb4
-rw-r--r--app/services/projects/update_statistics_service.rb19
-rw-r--r--app/services/releases/concerns.rb2
-rw-r--r--app/services/releases/create_service.rb20
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/views/admin/users/_user_detail.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml7
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/project_cache_worker.rb20
-rw-r--r--app/workers/update_project_statistics_worker.rb18
-rw-r--r--changelogs/unreleased/30157-api-expose-single-environment.yml5
-rw-r--r--changelogs/unreleased/43263-git-push-option-to-create-mr.yml5
-rw-r--r--changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml6
-rw-r--r--changelogs/unreleased/53279-fix-updated_at-api.yml5
-rw-r--r--changelogs/unreleased/59708-vendor-css.yml5
-rw-r--r--changelogs/unreleased/delay-update-statictics.yml5
-rw-r--r--changelogs/unreleased/fix-pull-request-importer.yml5
-rw-r--r--changelogs/unreleased/instance-configuration-artifact-size.yml5
-rw-r--r--changelogs/unreleased/issue-58418-release-notes.yml5
-rw-r--r--changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml5
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--db/migrate/20180115094742_add_default_project_creation_setting.rb19
-rw-r--r--db/migrate/20180115113902_add_project_creation_level_to_groups.rb17
-rw-r--r--db/migrate/20190311132500_add_default_project_creation_application_setting.rb22
-rw-r--r--db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb22
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/auth/okta.md7
-rw-r--r--doc/api/commits.md27
-rw-r--r--doc/api/environments.md105
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/chatops/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md35
-rw-r--r--doc/ci/docker/using_docker_images.md3
-rw-r--r--doc/ci/environments.md2
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/ci/examples/test-scala-application.md2
-rw-r--r--doc/ci/large_repositories/index.md235
-rw-r--r--doc/ci/ssh_keys/README.md4
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md19
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md2
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/development/documentation/index.md48
-rw-r--r--doc/development/documentation/styleguide.md55
-rw-r--r--doc/development/ee_features.md5
-rw-r--r--doc/development/fe_guide/vue.md6
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/gitaly.md2
-rw-r--r--doc/development/testing_guide/best_practices.md6
-rw-r--r--doc/gitlab-basics/create-issue.md4
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/topics/autodevops/index.md6
-rw-r--r--doc/user/group/index.md2
-rw-r--r--doc/user/index.md4
-rw-r--r--doc/user/markdown.md38
-rw-r--r--doc/user/project/clusters/index.md4
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/issues/create_new_issue.md2
-rw-r--r--doc/user/project/issues/index.md227
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md (renamed from doc/user/project/issues/issues_functionalities.md)6
-rw-r--r--doc/user/project/merge_requests/index.md60
-rw-r--r--doc/user/project/pages/lets_encrypt_for_gitlab_pages.md2
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--lib/api/entities.rb9
-rw-r--r--lib/api/environments.rb15
-rw-r--r--lib/api/helpers/internal_helpers.rb22
-rw-r--r--lib/api/internal.rb24
-rw-r--r--lib/api/issues.rb10
-rw-r--r--lib/gitlab/background_migration.rb28
-rw-r--r--lib/gitlab/background_migration/migrate_stage_index.rb4
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb4
-rw-r--r--lib/gitlab/data_builder/push.rb9
-rw-r--r--lib/gitlab/git_post_receive.rb2
-rw-r--r--lib/gitlab/import/merge_request_helpers.rb2
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb1
-rw-r--r--lib/gitlab/push_options.rb70
-rw-r--r--locale/gitlab.pot99
-rw-r--r--package.json4
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb4
-rw-r--r--qa/spec/spec_helper.rb2
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb11
-rw-r--r--spec/controllers/projects/tags/releases_controller_spec.rb61
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb2
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb29
-rw-r--r--spec/features/labels_hierarchy_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb2
-rw-r--r--spec/fixtures/api/graphql/introspection.graphql92
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact_file.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deployment.json32
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/environment.json23
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json63
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/runner.json24
-rw-r--r--spec/frontend/.eslintrc.yml3
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js2
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_error_spec.js2
-rw-r--r--spec/frontend/ide/lib/common/disposable_spec.js2
-rw-r--r--spec/frontend/ide/lib/editor_options_spec.js2
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js4
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js14
-rw-r--r--spec/frontend/serverless/components/functions_spec.js14
-rw-r--r--spec/javascripts/monitoring/utils_spec.js29
-rw-r--r--spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/import/merge_request_helpers_spec.rb73
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb1
-rw-r--r--spec/lib/gitlab/push_options_spec.rb103
-rw-r--r--spec/models/instance_configuration_spec.rb7
-rw-r--r--spec/models/release_spec.rb16
-rw-r--r--spec/requests/api/environments_spec.rb22
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb16
-rw-r--r--spec/requests/api/internal_spec.rb101
-rw-r--r--spec/requests/api/runner_spec.rb2
-rw-r--r--spec/services/after_branch_delete_service_spec.rb15
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb3
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb339
-rw-r--r--spec/services/git/branch_push_service_spec.rb258
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb144
-rw-r--r--spec/services/git/tag_push_service_spec.rb179
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb404
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb34
-rw-r--r--spec/services/note_summary_spec.rb10
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb40
-rw-r--r--spec/services/releases/create_service_spec.rb10
-rw-r--r--spec/services/system_note_service_spec.rb83
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/wiki_file_attachments_examples.rb2
-rw-r--r--spec/workers/post_receive_spec.rb25
-rw-r--r--spec/workers/project_cache_worker_spec.rb47
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb17
-rw-r--r--yarn.lock8
198 files changed, 4102 insertions, 1755 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 98fdda3593e..7d01afc9a16 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,12 @@ include:
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
.dedicated-runner: &dedicated-runner
- retry: 1
+ retry:
+ max: 1 # This is confusing but this means "2 runs at max".
+ when:
+ - unknown_failure
+ - api_failure
+ - runner_system_failure
tags:
- gitlab-org
@@ -76,6 +81,11 @@ stages:
- postgres:9.6
- redis:alpine
+.use-pg-10: &use-pg-10
+ services:
+ - postgres:10.0
+ - redis:alpine
+
.use-mysql: &use-mysql
services:
- mysql:5.7
@@ -97,6 +107,15 @@ stages:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
+.only-schedules-master: &only-schedules-master
+ only:
+ - schedules@gitlab-org/gitlab-ce
+ - schedules@gitlab-org/gitlab-ee
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
+
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
@@ -172,6 +191,11 @@ stages:
<<: *rspec-metadata
<<: *use-pg
+.rspec-metadata-pg-10: &rspec-metadata-pg-10
+ <<: *rspec-metadata
+ <<: *use-pg-10
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
+
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
@@ -457,6 +481,8 @@ setup-test-env:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
+ variables:
+ - $REVIEW_APP_CLEANUP
except:
refs:
- tags
@@ -525,8 +551,14 @@ rspec-pg:
<<: *rspec-metadata-pg
parallel: 50
+rspec-pg-10:
+ <<: *rspec-metadata-pg-10
+ <<: *only-schedules-master
+ parallel: 50
+
rspec-mysql:
<<: *rspec-metadata-mysql
+ <<: *only-schedules-master
parallel: 50
.rspec-quarantine: &rspec-quarantine
@@ -1141,4 +1173,3 @@ schedule:review-performance:
<<: *review-schedules-only
script:
- wait_for_job_to_be_done "schedule:review-deploy"
-
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index c14d69c5d18..6b54e8baefb 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,6 +1,8 @@
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
+import { isEE } from '~/lib/utils/common_utils';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@@ -8,6 +10,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
+ isGroup: isEE(),
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
this.store = store;
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index a8ee3f4ac10..70b5c6b0094 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -50,7 +50,7 @@ export default {
},
modalText() {
- const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`;
+ const linkStart = `<a class="commit-sha mr-0" href="${_.escape(this.commitUrl)}">`;
const commitId = _.escape(this.commitShortSha);
const linkEnd = '</a>';
const name = _.escape(this.name);
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index c541ea3445b..f0e80cba753 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -504,22 +504,28 @@ export default {
class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
- <span v-if="shouldRenderDeploymentID"> {{ deploymentInternalId }} </span>
+ <span v-if="shouldRenderDeploymentID" class="text-break-word">
+ {{ deploymentInternalId }}
+ </span>
- <span v-if="!model.isFolder && deploymentHasUser">
+ <span v-if="!model.isFolder && deploymentHasUser" class="text-break-word">
by
<user-avatar-link
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
- class="js-deploy-user-container"
+ class="js-deploy-user-container float-none"
/>
</span>
</div>
<div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell">
- <a v-if="shouldRenderBuildName" :href="buildPath" class="build-link flex-truncate-parent">
+ <a
+ v-if="shouldRenderBuildName"
+ :href="buildPath"
+ class="build-link cgray flex-truncate-parent"
+ >
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 266cdc42518..bafbc00597e 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -72,10 +72,9 @@ export default {
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- variant="secondary"
:disabled="isLoading"
:title="title"
- class="d-none d-md-block"
+ class="d-none d-md-block text-secondary"
@click="onClick"
>
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 6d74d136a94..13195d32cc4 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -39,7 +39,7 @@ export default {
:aria-label="title"
:href="terminalPath"
:class="{ disabled: disabled }"
- class="btn terminal-button d-none d-sm-none d-md-block"
+ class="btn terminal-button d-none d-sm-none d-md-block text-secondary"
>
<icon name="terminal" />
</a>
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index a4dad6f1615..37b5409a51d 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -6,5 +6,11 @@ export function resetServiceWorkersPublicPath() {
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+
+ // monaco-editor-webpack-plugin currently (incorrectly) references the
+ // public path as a property of `window`. Once this is fixed upstream we
+ // can remove this line
+ // see: https://github.com/Microsoft/monaco-editor-webpack-plugin/pull/63
window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line
}
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 9e5d0d0fd28..e97320fd682 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -18,5 +18,3 @@ export const timeWindows = {
threeDays: __('3 days'),
oneWeek: __('1 week'),
};
-
-export const msPerMinute = 60000;
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index e379827b769..ef309c8a398 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,4 +1,4 @@
-import { timeWindows, msPerMinute } from './constants';
+import { timeWindows } from './constants';
/**
* method that converts a predetermined time window to minutes
@@ -6,27 +6,26 @@ import { timeWindows, msPerMinute } from './constants';
* @param {String} timeWindow - The time window to convert to minutes
* @returns {number} The time window in minutes
*/
-const getTimeDifferenceMinutes = timeWindow => {
+const getTimeDifferenceSeconds = timeWindow => {
switch (timeWindow) {
case timeWindows.thirtyMinutes:
- return 30;
+ return 60 * 30;
case timeWindows.threeHours:
- return 60 * 3;
+ return 60 * 60 * 3;
case timeWindows.oneDay:
- return 60 * 24 * 1;
+ return 60 * 60 * 24 * 1;
case timeWindows.threeDays:
- return 60 * 24 * 3;
+ return 60 * 60 * 24 * 3;
case timeWindows.oneWeek:
- return 60 * 24 * 7 * 1;
+ return 60 * 60 * 24 * 7 * 1;
default:
- return 60 * 8;
+ return 60 * 60 * 8;
}
};
export const getTimeDiff = selectedTimeWindow => {
- const end = Date.now();
- const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow);
- const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime();
+ const end = Date.now() / 1000; // convert milliseconds to seconds
+ const start = end - getTimeDifferenceSeconds(selectedTimeWindow);
return { start, end };
};
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index ff303d0f55a..fbf75ed0e41 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions } from 'vuex';
import $ from 'jquery';
+import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
@@ -16,7 +17,7 @@ export default {
noteForm,
Suggestions,
},
- mixins: [autosave],
+ mixins: [autosave, getDiscussion],
props: {
note: {
type: Object,
@@ -76,8 +77,8 @@ export default {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
- handleFormUpdate(note, parentElement, callback) {
- this.$emit('handleFormUpdate', note, parentElement, callback);
+ handleFormUpdate(note, parentElement, callback, resolveDiscussion) {
+ this.$emit('handleFormUpdate', note, parentElement, callback, resolveDiscussion);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelForm', shouldConfirm, isDirty);
@@ -111,6 +112,8 @@ export default {
:line="line"
:note="note"
:help-page-path="helpPagePath"
+ :discussion="discussion"
+ :resolve-discussion="note.resolve_discussion"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index d2cfeff53e8..47d74c2f892 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -23,7 +24,7 @@ export default {
noteBody,
TimelineEntryItem,
},
- mixins: [noteable, resolvable],
+ mixins: [noteable, resolvable, draftMixin],
props: {
note: {
type: Object,
@@ -73,9 +74,6 @@ export default {
'is-editable': this.note.current_user.can_edit,
};
},
- canResolve() {
- return this.note.resolvable && !!this.getUserData.id;
- },
canReportAsAbuse() {
return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
@@ -156,12 +154,16 @@ export default {
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
- formUpdateHandler(noteText, parentElement, callback) {
+ formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
+ resolveDiscussion,
callback: () => this.updateSuccess(),
});
+
+ if (this.isDraft) return;
+
const data = {
endpoint: this.note.path,
note: {
@@ -234,6 +236,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
+ <slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span>
<span v-else class="d-none d-sm-inline">&middot;</span>
</note-header>
@@ -247,12 +250,15 @@ export default {
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
- :can-resolve="note.current_user.can_resolve"
+ :can-resolve="canResolve"
:report-abuse-path="note.report_abuse_path"
- :resolvable="note.resolvable"
- :is-resolved="note.resolved"
+ :resolvable="note.resolvable || note.isDraft"
+ :is-resolved="note.resolved || note.resolve_discussion"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
+ :is-draft="note.isDraft"
+ :resolve-discussion="note.isDraft && note.resolve_discussion"
+ :discussion-id="discussionId"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
diff --git a/app/assets/javascripts/notes/mixins/draft.js b/app/assets/javascripts/notes/mixins/draft.js
new file mode 100644
index 00000000000..1370f3978df
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/draft.js
@@ -0,0 +1,8 @@
+export default {
+ computed: {
+ isDraft: () => false,
+ canResolve() {
+ return this.note.current_user.can_resolve;
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/mixins/get_discussion.js b/app/assets/javascripts/notes/mixins/get_discussion.js
new file mode 100644
index 00000000000..b5d820fe083
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/get_discussion.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ discussion() {
+ return {};
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 3f282138bdf..944b9c0c083 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -162,7 +162,7 @@ export default {
</template>
<icon name="commit" class="commit-icon js-commit-icon" />
- <gl-link :href="commitUrl" class="commit-sha"> {{ shortSha }} </gl-link>
+ <gl-link :href="commitUrl" class="commit-sha mr-0"> {{ shortSha }} </gl-link>
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 8aaa9772715..a2f518cd24e 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -28,6 +28,9 @@
// Component specific styles, will be moved to gitlab-ui
@import "components/**/*";
+// Vendors specific styles
+@import "vendors/**/*";
+
// Styles for JS behaviors.
@import "behaviors";
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 7c10de828cd..bfd96a4bc05 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -163,88 +163,6 @@
}
}
-.atwho-view {
- overflow-y: auto;
- overflow-x: hidden;
-
- .name,
- small.aliases,
- small.params {
- float: left;
- }
-
- small.aliases,
- small.params {
- padding: 2px 5px;
- }
-
- small.description {
- float: right;
- padding: 3px 5px;
- }
-
- .avatar-inline {
- margin-bottom: 0;
- }
-
- .has-warning {
- .name,
- .description {
- color: $orange-700;
- }
- }
-
- .cur {
- .avatar {
- @include disable-all-animation;
- border: 1px solid $white-light;
- }
- }
-
- ul > li {
- @include clearfix;
- white-space: nowrap;
- }
-
- // TODO: fallback to global style
- .atwho-view-ul {
- padding: 8px 1px;
-
- li {
- padding: 8px 16px;
- border: 0;
-
- &.cur {
- background-color: $gray-darker;
- color: $gl-text-color;
-
- small {
- color: inherit;
- }
-
- &.has-warning {
- color: $orange-700;
- background-color: $orange-100;
- }
- }
-
- div.avatar {
- display: inline-flex;
- justify-content: center;
- align-items: center;
-
- .center {
- line-height: 14px;
- }
- }
-
- strong {
- color: $gl-text-color;
- }
- }
- }
-}
-
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
@@ -269,15 +187,6 @@
}
@include media-breakpoint-down(xs) {
- .atwho-view-ul {
- width: 350px;
- }
-
- .atwho-view ul li {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
.referenced-users {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 8e1ee51628d..93dffb5ff09 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -12,34 +12,6 @@
.environments-container {
.ci-table {
- .deployment-column {
- > span {
- word-break: break-all;
- }
-
- .avatar {
- float: none;
- }
- }
-
- .btn-group {
- > .btn:not(.btn-danger) {
- color: $gl-text-color-secondary;
- }
-
- svg path {
- fill: $gl-text-color-secondary;
- }
-
- .dropdown {
- outline: none;
- }
- }
-
- .btn .text-center {
- display: inline;
- }
-
.commit-title {
margin: 0;
}
@@ -49,47 +21,16 @@
color: $gl-text-color-secondary;
}
- .dropdown-menu {
- .fa {
- margin-right: 6px;
- color: $gl-text-color-secondary;
- }
- }
-
.build-link,
.ref-name {
color: $gl-text-color;
}
- .stop-env-link,
- .external-url {
- color: $gl-text-color-secondary;
-
- .stop-env-icon {
- font-size: 14px;
- }
- }
-
- .deployment .build-column {
- .build-link {
- color: $gl-text-color;
- }
-
- .avatar {
- float: none;
- margin-right: 0;
- }
- }
-
.folder-icon {
margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
vertical-align: text-top;
-
- .fa:nth-child(1) {
- margin-right: 3px;
- }
}
.folder-name {
@@ -103,12 +44,6 @@
text-align: center;
}
- .branch-commit {
- .commit-sha {
- margin-right: 0;
- }
- }
-
.no-btn {
border: 0;
background: none;
@@ -168,11 +103,6 @@
opacity: 0.25;
}
-.prometheus-graph-overlay {
- fill: none;
- opacity: 0;
- pointer-events: all;
-}
.rect-text-metric {
fill: $white-light;
@@ -203,276 +133,10 @@
stroke: $gray-darkest;
}
-.prometheus-graphs {
- .dropdowns {
- .dropdown-menu-toggle {
- svg {
- position: absolute;
- right: 5%;
- top: 25%;
- }
- }
-
- .dropdown-menu-toggle,
- .dropdown-menu {
- width: 240px;
- }
- }
-}
-
.environments-actions {
.external-url,
.monitoring-url,
- .terminal-button,
- .stop-env-link {
+ .terminal-button {
width: 38px;
}
}
-
-.prometheus-panel {
- margin-top: 20px;
-}
-
-.prometheus-graph-group {
- display: flex;
- flex-wrap: wrap;
- padding: $gl-padding / 2;
-}
-
-.prometheus-graph {
- padding: $gl-padding / 2;
-}
-
-.prometheus-graph-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: $gl-padding-8;
-
- h5 {
- font-size: $gl-font-size-large;
- margin: 0;
- }
-}
-
-.prometheus-graph-cursor {
- position: absolute;
- background: $gray-600;
- width: 1px;
-}
-
-.prometheus-graph-flag {
- display: block;
- min-width: 160px;
- border: 0;
- box-shadow: 0 1px 4px 0 $black-transparent;
-
- h5 {
- padding: 0;
- margin: 0;
- font-size: 14px;
- line-height: 1.2;
- }
-
- .deploy-meta-content {
- border-bottom: 1px solid $white-dark;
-
- svg {
- height: 15px;
- vertical-align: bottom;
- }
- }
-
- &.popover {
- padding: 0;
-
- &.left {
- left: auto;
- right: 0;
- margin-right: 10px;
-
- > .arrow {
- right: -14px;
- border-left-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-left: 4px solid $gray-50;
- }
-
- .arrow-shadow {
- right: -3px;
- box-shadow: 1px 0 9px 0 $black-transparent;
- }
- }
-
- &.right {
- left: 0;
- right: auto;
- margin-left: 10px;
-
- > .arrow {
- left: -7px;
- border-right-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-right: 4px solid $gray-50;
- }
-
- .arrow-shadow {
- left: -3px;
- box-shadow: 1px 0 8px 0 $black-transparent;
- }
- }
-
- > .arrow {
- top: 10px;
- margin: 0;
- }
-
- .arrow-shadow {
- content: '';
- position: absolute;
- width: 7px;
- height: 7px;
- background-color: transparent;
- transform: rotate(45deg);
- top: 13px;
- }
-
- > .popover-title,
- > .popover-content,
- > .popover-header,
- > .popover-body {
- padding: 8px;
- font-size: 12px;
- white-space: nowrap;
- position: relative;
- }
-
- > .popover-title {
- background-color: $gray-50;
- border-radius: $border-radius-default $border-radius-default 0 0;
- }
- }
-
- strong {
- font-weight: 600;
- }
-}
-
-.prometheus-table {
- border-collapse: collapse;
- padding: 0;
- margin: 0;
-
- td {
- vertical-align: middle;
-
- + td {
- padding-left: 8px;
- vertical-align: top;
- }
- }
-
- .legend-metric-title {
- font-size: 12px;
- vertical-align: middle;
- }
-}
-
-.prometheus-svg-container {
- position: relative;
- height: 0;
- width: 100%;
- padding: 0;
- padding-bottom: 100%;
-
- .text-metric-usage {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 12px;
- }
-
- > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
-
- .label-axis-text {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 10px;
- }
-
- .legend-axis-text {
- fill: $black;
- }
-
- .tick {
- > line {
- stroke: $gray-darker;
- }
-
- > text {
- fill: $gray-600;
- font-size: 10px;
- }
- }
-
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
-
- .axis-tick {
- stroke: $gray-darker;
- }
-
- .deploy-info-text {
- dominant-baseline: text-before-edge;
- font-size: 12px;
- }
-
- .deploy-info-text-link {
- font-family: $monospace-font;
- fill: $blue-600;
-
- &:hover {
- fill: $blue-800;
- }
- }
-
- @include media-breakpoint-down(sm) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
- }
-
- .tick > text {
- font-size: 8px;
- }
- }
- }
-}
-
-.prometheus-table-row-highlight {
- background-color: $gray-100;
-}
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
new file mode 100644
index 00000000000..c03554b287f
--- /dev/null
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -0,0 +1,270 @@
+.prometheus-graphs {
+ .dropdowns {
+ .dropdown-menu-toggle {
+ svg {
+ position: absolute;
+ right: 5%;
+ top: 25%;
+ }
+ }
+
+ .dropdown-menu-toggle,
+ .dropdown-menu {
+ width: 240px;
+ }
+ }
+}
+
+.prometheus-panel {
+ margin-top: 20px;
+}
+
+.prometheus-graph-group {
+ display: flex;
+ flex-wrap: wrap;
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph {
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $gl-padding-8;
+
+ h5 {
+ font-size: $gl-font-size-large;
+ margin: 0;
+ }
+}
+
+.prometheus-graph-cursor {
+ position: absolute;
+ background: $gray-600;
+ width: 1px;
+}
+
+.prometheus-graph-flag {
+ display: block;
+ min-width: 160px;
+ border: 0;
+ box-shadow: 0 1px 4px 0 $black-transparent;
+
+ h5 {
+ padding: 0;
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.2;
+ }
+
+ .deploy-meta-content {
+ border-bottom: 1px solid $white-dark;
+
+ svg {
+ height: 15px;
+ vertical-align: bottom;
+ }
+ }
+
+ &.popover {
+ padding: 0;
+
+ &.left {
+ left: auto;
+ right: 0;
+ margin-right: 10px;
+
+ > .arrow {
+ right: -14px;
+ border-left-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-left: 4px solid $gray-50;
+ }
+
+ .arrow-shadow {
+ right: -3px;
+ box-shadow: 1px 0 9px 0 $black-transparent;
+ }
+ }
+
+ &.right {
+ left: 0;
+ right: auto;
+ margin-left: 10px;
+
+ > .arrow {
+ left: -7px;
+ border-right-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-right: 4px solid $gray-50;
+ }
+
+ .arrow-shadow {
+ left: -3px;
+ box-shadow: 1px 0 8px 0 $black-transparent;
+ }
+ }
+
+ > .arrow {
+ top: 10px;
+ margin: 0;
+ }
+
+ .arrow-shadow {
+ content: '';
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ background-color: transparent;
+ transform: rotate(45deg);
+ top: 13px;
+ }
+
+ > .popover-title,
+ > .popover-content,
+ > .popover-header,
+ > .popover-body {
+ padding: 8px;
+ font-size: 12px;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ > .popover-title {
+ background-color: $gray-50;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+ }
+
+ strong {
+ font-weight: 600;
+ }
+}
+
+.prometheus-table {
+ border-collapse: collapse;
+ padding: 0;
+ margin: 0;
+
+ td {
+ vertical-align: middle;
+
+ + td {
+ padding-left: 8px;
+ vertical-align: top;
+ }
+ }
+
+ .legend-metric-title {
+ font-size: 12px;
+ vertical-align: middle;
+ }
+}
+
+.prometheus-svg-container {
+ position: relative;
+ height: 0;
+ width: 100%;
+ padding: 0;
+ padding-bottom: 100%;
+
+ .text-metric-usage {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 12px;
+ }
+
+ > svg {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
+
+ text {
+ fill: $gl-text-color;
+ stroke-width: 0;
+ }
+
+ .text-metric-bold {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
+ }
+
+ .legend-axis-text {
+ fill: $black;
+ }
+
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
+
+ > text {
+ fill: $gray-600;
+ font-size: 10px;
+ }
+ }
+
+ .y-label-text,
+ .x-label-text {
+ fill: $gray-darkest;
+ }
+
+ .axis-tick {
+ stroke: $gray-darker;
+ }
+
+ .deploy-info-text {
+ dominant-baseline: text-before-edge;
+ font-size: 12px;
+ }
+
+ .deploy-info-text-link {
+ font-family: $monospace-font;
+ fill: $blue-600;
+
+ &:hover {
+ fill: $blue-800;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ .label-axis-text,
+ .text-metric-usage,
+ .legend-axis-text {
+ font-size: 8px;
+ }
+
+ .tick > text {
+ font-size: 8px;
+ }
+ }
+ }
+}
+
+.prometheus-table-row-highlight {
+ background-color: $gray-100;
+}
+
+.prometheus-graph-overlay {
+ fill: none;
+ opacity: 0;
+ pointer-events: all;
+}
diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss
new file mode 100644
index 00000000000..ccf3824ea56
--- /dev/null
+++ b/app/assets/stylesheets/vendors/atwho.scss
@@ -0,0 +1,92 @@
+.atwho-view {
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ .name,
+ small.aliases,
+ small.params {
+ float: left;
+ }
+
+ small.aliases,
+ small.params {
+ padding: 2px 5px;
+ }
+
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .has-warning {
+ .name,
+ .description {
+ color: $orange-700;
+ }
+ }
+
+ .cur {
+ .avatar {
+ @include disable-all-animation;
+ border: 1px solid $white-light;
+ }
+ }
+
+ ul > li {
+ @include clearfix;
+ white-space: nowrap;
+ }
+
+ // TODO: fallback to global style
+ .atwho-view-ul {
+ padding: 8px 1px;
+
+ li {
+ padding: 8px 16px;
+ border: 0;
+
+ &.cur {
+ background-color: $gray-darker;
+ color: $gl-text-color;
+
+ small {
+ color: inherit;
+ }
+
+ &.has-warning {
+ color: $orange-700;
+ background-color: $orange-100;
+ }
+ }
+
+ div.avatar {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ .center {
+ line-height: 14px;
+ }
+ }
+
+ strong {
+ color: $gl-text-color;
+ }
+ }
+ }
+}
+
+@include media-breakpoint-down(xs) {
+ .atwho-view-ul {
+ width: 350px;
+ }
+
+ .atwho-view ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 68e14f0c2e5..7d8016f763d 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -16,7 +16,7 @@ class AbuseReportsController < ApplicationController
if @abuse_report.save
@abuse_report.notify
- message = "Thank you for your report. A GitLab administrator will look into it shortly."
+ message = _("Thank you for your report. A GitLab administrator will look into it shortly.")
redirect_to @abuse_report.user, notice: message
else
render :new
@@ -37,9 +37,9 @@ class AbuseReportsController < ApplicationController
@user = User.find_by(id: params[:user_id])
if @user.nil?
- redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted."
+ redirect_to root_path, alert: _("Cannot create the abuse report. The user has been deleted.")
elsif @user.blocked?
- redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked."
+ redirect_to @user, alert: _("Cannot create the abuse report. This user has been blocked.")
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b7eb6af6d67..d5f1e35a79b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -293,7 +293,7 @@ class ApplicationController < ActionController::Base
unless Gitlab::Auth::LDAP::Access.allowed?(current_user)
sign_out current_user
- flash[:alert] = "Access denied for your LDAP account."
+ flash[:alert] = _("Access denied for your LDAP account.")
redirect_to new_user_session_path
end
end
@@ -340,7 +340,7 @@ class ApplicationController < ActionController::Base
def require_email
if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
- return redirect_to profile_path, notice: 'Please complete your profile with email address'
+ return redirect_to profile_path, notice: _('Please complete your profile with email address')
end
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 2c4aab67448..2ae500a2fdf 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -22,7 +22,7 @@ class ConfirmationsController < Devise::ConfirmationsController
after_sign_in(resource)
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
- flash[:notice] = flash[:notice] + " Please sign in."
+ flash[:notice] = flash[:notice] + _(" Please sign in.")
new_session_path(:user, anchor: 'login-pane')
end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index a9d6addd4a4..10cdce98437 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -22,7 +22,7 @@ class HelpController < ApplicationController
end
def show
- @path = clean_path_info(path_params[:path])
+ @path = Rack::Utils.clean_path_info(path_params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
@@ -75,35 +75,4 @@ class HelpController < ApplicationController
params
end
-
- PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
-
- # Taken from ActionDispatch::FileHandler
- # Cleans up the path, to prevent directory traversal outside the doc folder.
- def clean_path_info(path_info)
- parts = path_info.split(PATH_SEPS)
-
- clean = []
-
- # Walk over each part of the path
- parts.each do |part|
- # Turn `one//two` or `one/./two` into `one/two`.
- next if part.empty? || part == '.'
-
- if part == '..'
- # Turn `one/two/../` into `one`
- clean.pop
- else
- # Add simple folder names to the clean path.
- clean << part
- end
- end
-
- # If the path was an absolute path (i.e. `/` or `/one/two`),
- # add `/` to the front of the clean path.
- clean.unshift '/' if parts.empty? || parts.first.empty?
-
- # Join all the clean path parts by the path separator.
- ::File.join(*clean)
- end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 315d1375e02..a78d87eceea 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -13,9 +13,9 @@ class InvitesController < ApplicationController
if member.accept_invite!(current_user)
label, path = source_info(member.source)
- redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
+ redirect_to path, notice: _("You have been granted %{member_human_access} access to %{label}.") % { member_human_access: member.human_access, label: label }
else
- redirect_back_or_default(options: { alert: "The invitation could not be accepted." })
+ redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
end
end
@@ -30,9 +30,9 @@ class InvitesController < ApplicationController
new_user_session_path
end
- redirect_to path, notice: "You have declined the invitation to join #{label}."
+ redirect_to path, notice: _("You have declined the invitation to join %{label}.") % { label: label }
else
- redirect_back_or_default(options: { alert: "The invitation could not be declined." })
+ redirect_back_or_default(options: { alert: _("The invitation could not be declined.") })
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index e90e8278c13..d9b3b4bbbd9 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -105,11 +105,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def redirect_identity_link_failed(error_message)
- redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
+ redirect_to profile_account_path, notice: _("Authentication failed: %{error_message}") % { error_message: error_message }
end
def redirect_identity_linked
- redirect_to profile_account_path, notice: 'Authentication method updated'
+ redirect_to profile_account_path, notice: _('Authentication method updated')
end
def handle_service_ticket(provider, ticket)
@@ -147,10 +147,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_signup_error
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
- message = ["Signing in using your #{label} account without a pre-existing GitLab account is not allowed."]
+ message = [_("Signing in using your %{label} account without a pre-existing GitLab account is not allowed.") % { label: label }]
if Gitlab::CurrentSettings.allow_signup?
- message << "Create a GitLab account first, and then connect it to your #{label} account."
+ message << _("Create a GitLab account first, and then connect it to your %{label} account.") % { label: label }
end
flash[:notice] = message.join(' ')
@@ -168,14 +168,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def fail_auth0_login
- flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
+ flash[:alert] = _('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
redirect_to new_user_session_path
end
def handle_disabled_provider
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
- flash[:alert] = "Signing in using #{label} has been disabled"
+ flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }
redirect_to new_user_session_path
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 28f113b5cbe..77de5cb45c9 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -22,7 +22,7 @@ class PasswordsController < Devise::PasswordsController
).first_or_initialize
unless user.reset_password_period_valid?
- flash[:alert] = 'Your password reset token has expired.'
+ flash[:alert] = _('Your password reset token has expired.')
redirect_to(new_user_password_url(user_email: user['email']))
end
end
@@ -52,7 +52,7 @@ class PasswordsController < Devise::PasswordsController
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
- alert: "Password authentication is unavailable."
+ alert: _("Password authentication is unavailable.")
end
def throttle_reset
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index 2e78b9e6dc7..80b8279e91e 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -15,9 +15,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
new_chat_name = current_user.chat_names.new(chat_name_params)
if new_chat_name.save
- flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ flash[:notice] = _("Authorized %{new_chat_name}") % { new_chat_name: new_chat_name.chat_name }
else
- flash[:alert] = "Could not authorize chat nickname. Try again!"
+ flash[:alert] = _("Could not authorize chat nickname. Try again!")
end
delete_chat_name_token
@@ -27,7 +27,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
def deny
delete_chat_name_token
- flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+ flash[:notice] = _("Denied authorization of chat nickname %{user_name}.") % { user_name: chat_name_params[:user_name] }
redirect_to profile_chat_names_path
end
@@ -36,9 +36,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
@chat_name = chat_names.find(params[:id])
if @chat_name.destroy
- flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ flash[:notice] = _("Deleted chat nickname: %{chat_name}!") % { chat_name: @chat_name.chat_name }
else
- flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ flash[:alert] = _("Could not delete chat nickname %{chat_name}.") % { chat_name: @chat_name.chat_name }
end
redirect_to profile_chat_names_path, status: :found
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4b6ec2697b7..213d900a563 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -11,7 +11,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
if @personal_access_token.save
PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
- redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
else
set_index_vars
render :index
@@ -22,9 +22,9 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = finder.find(params[:id])
if @personal_access_token.revoke!
- flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ flash[:notice] = _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: @personal_access_token.name }
else
- flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ flash[:alert] = _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: @personal_access_token.name }
end
redirect_to profile_personal_access_tokens_path
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 0227af2c266..0e30df1b15b 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -11,13 +11,13 @@ class Profiles::PreferencesController < Profiles::ApplicationController
result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success
- flash[:notice] = 'Preferences saved.'
+ flash[:notice] = _('Preferences saved.')
else
- flash[:alert] = 'Failed to save preferences.'
+ flash[:alert] = _('Failed to save preferences.')
end
rescue ArgumentError => e
# Raised when `dashboard` is given an invalid value.
- flash[:alert] = "Failed to save preferences (#{e.message})."
+ flash[:alert] = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
end
respond_to do |format|
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
index e6a154fb6aa..866c4dee6e2 100644
--- a/app/controllers/profiles/u2f_registrations_controller.rb
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -4,6 +4,6 @@ class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
u2f_registration.destroy
- redirect_to profile_two_factor_auth_path, status: 302, notice: "Successfully deleted U2F device."
+ redirect_to profile_two_factor_auth_path, status: 302, notice: _("Successfully deleted U2F device.")
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 301449cfa90..e35f34be23c 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -193,7 +193,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
return unless Feature.enabled?(:metrics_time_window, project)
return unless params[:start].present? || params[:end].present?
- params.require([:start, :end]).values_at(:start, :end)
+ params.require([:start, :end])
end
def search_environment_names
diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb
index 334e1847cc8..5e4c601a693 100644
--- a/app/controllers/projects/tags/releases_controller.rb
+++ b/app/controllers/projects/tags/releases_controller.rb
@@ -12,16 +12,13 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
end
def update
- # Release belongs to Tag which is not active record object,
- # it exists only to save a description to each Tag.
- # If description is empty we should destroy the existing record.
if release_params[:description].present?
release.update(release_params)
else
release.destroy
end
- redirect_to project_tag_path(@project, @tag.name)
+ redirect_to project_tag_path(@project, tag.name)
end
private
@@ -30,11 +27,10 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
@tag ||= @repository.find_tag(params[:tag_id])
end
- # rubocop: disable CodeReuse/ActiveRecord
def release
- @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
+ @release ||= Releases::CreateService.new(project, current_user, tag: @tag.name)
+ .find_or_build_release
end
- # rubocop: enable CodeReuse/ActiveRecord
def release_params
params.require(:release).permit(:description)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 8b8d87524a8..0fa4677ced1 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -27,7 +27,7 @@ class RegistrationsController < Devise::RegistrationsController
persist_accepted_terms_if_required(new_user)
end
else
- flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash[:alert] = s_('Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
render action: 'new'
end
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 2b76921ebd8..77757c4a3ef 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -16,7 +16,7 @@ class SentNotificationsController < ApplicationController
noteable = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
- flash[:notice] = "You have been unsubscribed from this thread."
+ flash[:notice] = _("You have been unsubscribed from this thread.")
if current_user
redirect_to noteable_path(noteable)
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4bd7d71e264..6943795e8ac 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -70,7 +70,7 @@ class SessionsController < Devise::SessionsController
increment_failed_login_captcha_counter
self.resource = resource_class.new
- flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
respond_with_navigational(resource) { render :new }
@@ -122,7 +122,7 @@ class SessionsController < Devise::SessionsController
end
redirect_to edit_user_password_path(reset_password_token: @token),
- notice: "Please create a password for your new account."
+ notice: _("Please create a password for your new account.")
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 53efd9042b1..1afe000c5f8 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
class GitlabSchema < GraphQL::Schema
- # Took our current most complicated query in use, issues.graphql,
- # with a complexity of 19, and added a 20 point buffer to it.
+ # Currently an IntrospectionQuery has a complexity of 179.
# These values will evolve over time.
- DEFAULT_MAX_COMPLEXITY = 40
- AUTHENTICATED_COMPLEXITY = 50
- ADMIN_COMPLEXITY = 60
+ DEFAULT_MAX_COMPLEXITY = 200
+ AUTHENTICATED_COMPLEXITY = 250
+ ADMIN_COMPLEXITY = 300
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 3c74034b527..423ce7e7db1 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -13,6 +13,14 @@ module Noteable
end
end
+ # The timestamp of the note (e.g. the :updated_at attribute if provided via
+ # API call)
+ def system_note_timestamp
+ @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ attr_writer :system_note_timestamp
+
def base_class_name
self.class.base_class.name
end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 7f9ff7bbda6..46cac1d41bb 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -38,6 +38,15 @@ class GpgSignature < ApplicationRecord
.safe_find_or_create_by!(commit_sha: attributes[:commit_sha])
end
+ # Find commits that are lacking a signature in the database at present
+ def self.unsigned_commit_shas(commit_shas)
+ return [] if commit_shas.empty?
+
+ signed = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
+
+ commit_shas - signed
+ end
+
def gpg_key=(model)
case model
when GpgKey
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 11289887e00..a9b1962f24c 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -39,7 +39,7 @@ class InstanceConfiguration
def gitlab_ci
Settings.gitlab_ci
.to_h
- .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
+ .merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes,
default: 100.megabytes })
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 97c6dcc4745..261935fd054 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -90,7 +90,7 @@ class Issue < ApplicationRecord
state :closed
before_transition any => :closed do |issue|
- issue.closed_at = Time.zone.now
+ issue.closed_at = issue.system_note_timestamp
end
before_transition closed: :opened do |issue|
diff --git a/app/models/release.rb b/app/models/release.rb
index 746fc31a038..0f9e94373c7 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -15,6 +15,7 @@ class Release < ApplicationRecord
accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true
+ validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) }
diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb
deleted file mode 100644
index ece9fbbef43..00000000000
--- a/app/services/after_branch_delete_service.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# Branch can be deleted either by DeleteBranchService or by Git::BranchPushService.
-class AfterBranchDeleteService < BaseService
- attr_reader :branch_name
-
- def execute(branch_name)
- @branch_name = branch_name
-
- stop_environments
- end
-
- private
-
- def stop_environments
- Ci::StopEnvironmentsService
- .new(project, current_user)
- .execute(branch_name)
- end
-end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 8973c5ffc9e..41dee4e5641 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -37,7 +37,7 @@ module Ci
variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user,
- push_options: params[:push_options],
+ push_options: params[:push_options] || {},
chat_data: params[:chat_data],
**extra_options(options))
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 8322a3d74f4..4c3ac19f754 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -29,14 +29,4 @@ class DeleteBranchService < BaseService
def success(message)
super().merge(message: message)
end
-
- def build_push_data(branch)
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- branch.dereferenced_target.sha,
- Gitlab::Git::BLANK_SHA,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
- [])
- end
end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
new file mode 100644
index 00000000000..fce4040e390
--- /dev/null
+++ b/app/services/git/base_hooks_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Git
+ class BaseHooksService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ # The N most recent commits to process in a single push payload.
+ PROCESS_COMMIT_LIMIT = 100
+
+ def execute
+ project.repository.after_create if project.empty_repo?
+
+ create_events
+ create_pipelines
+ execute_project_hooks
+
+ # Not a hook, but it needs access to the list of changed commits
+ enqueue_invalidate_cache
+
+ push_data
+ end
+
+ private
+
+ def hook_name
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def commits
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def limited_commits
+ commits.last(PROCESS_COMMIT_LIMIT)
+ end
+
+ def commits_count
+ commits.count
+ end
+
+ def event_message
+ nil
+ end
+
+ def invalidated_file_types
+ []
+ end
+
+ def create_events
+ EventCreateService.new.push(project, current_user, push_data)
+ end
+
+ def create_pipelines
+ Ci::CreatePipelineService
+ .new(project, current_user, push_data)
+ .execute(:push, pipeline_options)
+ end
+
+ def execute_project_hooks
+ project.execute_hooks(push_data, hook_name)
+ project.execute_services(push_data, hook_name)
+ end
+
+ def enqueue_invalidate_cache
+ ProjectCacheWorker.perform_async(
+ project.id,
+ invalidated_file_types,
+ [:commit_count, :repository_size]
+ )
+ end
+
+ def push_data
+ @push_data ||= Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ limited_commits,
+ event_message,
+ commits_count: commits_count,
+ push_options: params[:push_options] || {}
+ )
+
+ # Dependent code may modify the push data, so return a duplicate each time
+ @push_data.dup
+ end
+
+ # to be overridden in EE
+ def pipeline_options
+ {}
+ end
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
new file mode 100644
index 00000000000..d21a6bb1b9a
--- /dev/null
+++ b/app/services/git/branch_hooks_service.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+module Git
+ class BranchHooksService < ::Git::BaseHooksService
+ def execute
+ execute_branch_hooks
+
+ super.tap do
+ enqueue_update_gpg_signatures
+ end
+ end
+
+ private
+
+ def hook_name
+ :push_hooks
+ end
+
+ def commits
+ strong_memoize(:commits) do
+ if creating_default_branch?
+ # The most recent PROCESS_COMMIT_LIMIT commits in the default branch
+ offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max
+ project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
+ elsif creating_branch?
+ # Use the pushed commits that aren't reachable by the default branch
+ # as a heuristic. This may include more commits than are actually
+ # pushed, but that shouldn't matter because we check for existing
+ # cross-references later.
+ project.repository.commits_between(project.default_branch, params[:newrev])
+ elsif updating_branch?
+ project.repository.commits_between(params[:oldrev], params[:newrev])
+ else # removing branch
+ []
+ end
+ end
+ end
+
+ def commits_count
+ return count_commits_in_branch if creating_default_branch?
+
+ super
+ end
+
+ def invalidated_file_types
+ return super unless default_branch? && !creating_branch?
+
+ paths = limited_commits.each_with_object(Set.new) do |commit, set|
+ commit.raw_deltas.each do |diff|
+ set << diff.new_path
+ end
+ end
+
+ Gitlab::FileDetector.types_in_paths(paths)
+ end
+
+ def execute_branch_hooks
+ project.repository.after_push_commit(branch_name)
+
+ branch_create_hooks if creating_branch?
+ branch_update_hooks if updating_branch?
+ branch_change_hooks if creating_branch? || updating_branch?
+ branch_remove_hooks if removing_branch?
+ end
+
+ def branch_create_hooks
+ project.repository.after_create_branch
+ project.after_create_default_branch if default_branch?
+ end
+
+ def branch_update_hooks
+ # Update the bare repositories info/attributes file using the contents of
+ # the default branch's .gitattributes file
+ project.repository.copy_gitattributes(params[:ref]) if default_branch?
+ end
+
+ def branch_change_hooks
+ enqueue_process_commit_messages
+ end
+
+ def branch_remove_hooks
+ project.repository.after_remove_branch
+ end
+
+ # Schedules processing of commit messages
+ def enqueue_process_commit_messages
+ # don't process commits for the initial push to the default branch
+ return if creating_default_branch?
+
+ limited_commits.each do |commit|
+ next unless commit.matches_cross_reference_regex?
+
+ ProcessCommitWorker.perform_async(
+ project.id,
+ current_user.id,
+ commit.to_hash,
+ default_branch?
+ )
+ end
+ end
+
+ def enqueue_update_gpg_signatures
+ unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha))
+ return if unsigned.empty?
+
+ signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
+ return if signable.empty?
+
+ CreateGpgSignatureWorker.perform_async(signable, project.id)
+ end
+
+ def creating_branch?
+ Gitlab::Git.blank_ref?(params[:oldrev])
+ end
+
+ def updating_branch?
+ !creating_branch? && !removing_branch?
+ end
+
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
+ end
+
+ def creating_default_branch?
+ creating_branch? && default_branch?
+ end
+
+ def count_commits_in_branch
+ strong_memoize(:count_commits_in_branch) do
+ project.repository.commit_count_for_ref(params[:ref])
+ end
+ end
+
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
+ end
+ end
+
+ def branch_name
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ end
+ end
+end
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index b55aeb5f2b9..da053ce80c7 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -1,14 +1,10 @@
# frozen_string_literal: true
module Git
- class BranchPushService < BaseService
- attr_accessor :push_data, :push_commits
+ class BranchPushService < ::BaseService
include Gitlab::Access
include Gitlab::Utils::StrongMemoize
- # The N most recent commits to process in a single push payload.
- PROCESS_COMMIT_LIMIT = 100
-
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
#
@@ -23,108 +19,43 @@ module Git
# 6. Checks if the project's main language has changed
#
def execute
- update_commits
+ return unless Gitlab::Git.branch_ref?(params[:ref])
+
+ enqueue_update_mrs
+ enqueue_detect_repository_languages
+
execute_related_hooks
perform_housekeeping
update_remote_mirrors
- update_caches
+ stop_environments
- update_signatures
+ true
end
- def update_commits
- project.repository.after_create if project.empty_repo?
- project.repository.after_push_commit(branch_name)
-
- if push_remove_branch?
- project.repository.after_remove_branch
- @push_commits = []
- elsif push_to_new_branch?
- project.repository.after_create_branch
-
- # Re-find the pushed commits.
- if default_branch?
- # Initial push to the default branch. Take the full history of that branch as "newly pushed".
- process_default_branch
- else
- # Use the pushed commits that aren't reachable by the default branch
- # as a heuristic. This may include more commits than are actually pushed, but
- # that shouldn't matter because we check for existing cross-references later.
- @push_commits = project.repository.commits_between(project.default_branch, params[:newrev])
-
- # don't process commits for the initial push to the default branch
- process_commit_messages
- end
- elsif push_to_existing_branch?
- # Collect data for this git push
- @push_commits = project.repository.commits_between(params[:oldrev], params[:newrev])
-
- process_commit_messages
-
- # Update the bare repositories info/attributes file using the contents of the default branches
- # .gitattributes file
- update_gitattributes if default_branch?
- end
- end
-
- def update_gitattributes
- project.repository.copy_gitattributes(params[:ref])
- end
-
- def update_caches
- if default_branch?
- if push_to_new_branch?
- # If this is the initial push into the default branch, the file type caches
- # will already be reset as a result of `Project#change_head`.
- types = []
- else
- paths = Set.new
-
- last_pushed_commits.each do |commit|
- commit.raw_deltas.each do |diff|
- paths << diff.new_path
- end
- end
-
- types = Gitlab::FileDetector.types_in_paths(paths.to_a)
- end
-
- DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id)
- else
- types = []
- end
-
- ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size])
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ def enqueue_update_mrs
+ UpdateMergeRequestsWorker.perform_async(
+ project.id,
+ current_user.id,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref]
+ )
end
- # rubocop: disable CodeReuse/ActiveRecord
- def update_signatures
- commit_shas = last_pushed_commits.map(&:sha)
+ def enqueue_detect_repository_languages
+ return unless default_branch?
- return if commit_shas.empty?
-
- shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
- commit_shas -= shas_with_cached_signatures
-
- return if commit_shas.empty?
-
- commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas)
-
- CreateGpgSignatureWorker.perform_async(commit_shas, project.id)
+ DetectRepositoryLanguagesWorker.perform_async(project.id, current_user.id)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # Schedules processing of commit messages.
- def process_commit_messages
- default = default_branch?
+ # Only stop environments if the ref is a branch that is being deleted
+ def stop_environments
+ return unless removing_branch?
- last_pushed_commits.each do |commit|
- if commit.matches_cross_reference_regex?
- ProcessCommitWorker
- .perform_async(project.id, current_user.id, commit.to_hash, default)
- end
- end
+ Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name)
end
def update_remote_mirrors
@@ -135,23 +66,7 @@ module Git
end
def execute_related_hooks
- # Update merge requests that may be affected by this push. A new branch
- # could cause the last commit of a merge request to change.
- #
- UpdateMergeRequestsWorker
- .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
-
- EventCreateService.new.push(project, current_user, build_push_data)
- Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options)
-
- project.execute_hooks(build_push_data.dup, :push_hooks)
- project.execute_services(build_push_data.dup, :push_hooks)
-
- if push_remove_branch?
- AfterBranchDeleteService
- .new(project, current_user)
- .execute(branch_name)
- end
+ BranchHooksService.new(project, current_user, params).execute
end
def perform_housekeeping
@@ -161,84 +76,18 @@ module Git
rescue Projects::HousekeepingService::LeaseTaken
end
- def process_default_branch
- offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max
- @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
-
- project.after_create_default_branch
- end
-
- def build_push_data
- @push_data ||= Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- @push_commits,
- commits_count: commits_count,
- push_options: params[:push_options] || []
- )
- end
-
- def push_to_existing_branch?
- # Return if this is not a push to a branch (e.g. new commits)
- branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev])
- end
-
- def push_to_new_branch?
- strong_memoize(:push_to_new_branch) do
- branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev])
- end
- end
-
- def push_remove_branch?
- strong_memoize(:push_remove_branch) do
- branch_ref? && Gitlab::Git.blank_ref?(params[:newrev])
- end
- end
-
- def default_branch?
- branch_ref? &&
- (branch_name == project.default_branch || project.default_branch.nil?)
- end
-
- def commit_user(commit)
- commit.author || current_user
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
end
def branch_name
- strong_memoize(:branch_name) do
- Gitlab::Git.ref_name(params[:ref])
- end
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
end
- def branch_ref?
- strong_memoize(:branch_ref) do
- Gitlab::Git.branch_ref?(params[:ref])
- end
- end
-
- def commits_count
- return push_commits_count_for_ref if default_branch? && push_to_new_branch?
-
- Array(@push_commits).size
- end
-
- def push_commits_count_for_ref
- strong_memoize(:push_commits_count_for_ref) do
- project.repository.commit_count_for_ref(params[:ref])
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
end
end
-
- def last_pushed_commits
- @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT)
- end
-
- private
-
- def pipeline_options
- {} # to be overridden in EE
- end
end
end
diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb
new file mode 100644
index 00000000000..18eb780579f
--- /dev/null
+++ b/app/services/git/tag_hooks_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Git
+ class TagHooksService < ::Git::BaseHooksService
+ private
+
+ def hook_name
+ :tag_push_hooks
+ end
+
+ def commits
+ [tag_commit].compact
+ end
+
+ def event_message
+ tag&.message
+ end
+
+ def tag
+ strong_memoize(:tag) do
+ next if Gitlab::Git.blank_ref?(params[:newrev])
+
+ tag_name = Gitlab::Git.ref_name(params[:ref])
+ tag = project.repository.find_tag(tag_name)
+
+ tag if tag && tag.target == params[:newrev]
+ end
+ end
+
+ def tag_commit
+ strong_memoize(:tag_commit) do
+ project.commit(tag.dereferenced_target) if tag
+ end
+ end
+ end
+end
diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb
index 9ce0fbdb206..ee4166dccd0 100644
--- a/app/services/git/tag_push_service.rb
+++ b/app/services/git/tag_push_service.rb
@@ -1,56 +1,14 @@
# frozen_string_literal: true
module Git
- class TagPushService < BaseService
- attr_accessor :push_data
-
+ class TagPushService < ::BaseService
def execute
- project.repository.after_create if project.empty_repo?
- project.repository.before_push_tag
-
- @push_data = build_push_data
-
- EventCreateService.new.push(project, current_user, push_data)
- Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options)
+ return unless Gitlab::Git.tag_ref?(params[:ref])
- project.execute_hooks(push_data.dup, :tag_push_hooks)
- project.execute_services(push_data.dup, :tag_push_hooks)
-
- ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
+ project.repository.before_push_tag
+ TagHooksService.new(project, current_user, params).execute
true
end
-
- private
-
- def build_push_data
- commits = []
- message = nil
-
- unless Gitlab::Git.blank_ref?(params[:newrev])
- tag_name = Gitlab::Git.ref_name(params[:ref])
- tag = project.repository.find_tag(tag_name)
-
- if tag && tag.target == params[:newrev]
- commit = project.commit(tag.dereferenced_target)
- commits = [commit].compact
- message = tag.message
- end
- end
-
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- commits,
- message,
- push_options: params[:push_options] || [])
- end
-
- def pipeline_options
- {} # to be overridden in EE
- end
end
end
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
new file mode 100644
index 00000000000..d92eb0a68c3
--- /dev/null
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class PushOptionsHandlerService
+ LIMIT = 10
+
+ attr_reader :branches, :changes_by_branch, :current_user, :errors,
+ :project, :push_options, :target_project
+
+ def initialize(project, current_user, changes, push_options)
+ @project = project
+ @target_project = @project.default_merge_request_target
+ @current_user = current_user
+ @branches = get_branches(changes)
+ @push_options = push_options
+ @errors = []
+ end
+
+ def execute
+ validate_service
+ return self if errors.present?
+
+ branches.each do |branch|
+ execute_for_branch(branch)
+ rescue Gitlab::Access::AccessDeniedError
+ errors << 'User access was denied'
+ rescue StandardError => e
+ Gitlab::AppLogger.error(e)
+ errors << 'An unknown error occurred'
+ end
+
+ self
+ end
+
+ private
+
+ def get_branches(raw_changes)
+ Gitlab::ChangesList.new(raw_changes).map do |changes|
+ next unless Gitlab::Git.branch_ref?(changes[:ref])
+
+ # Deleted branch
+ next if Gitlab::Git.blank_ref?(changes[:newrev])
+
+ # Default branch
+ branch_name = Gitlab::Git.branch_name(changes[:ref])
+ next if branch_name == target_project.default_branch
+
+ branch_name
+ end.compact.uniq
+ end
+
+ def validate_service
+ errors << 'User is required' if current_user.nil?
+
+ unless target_project.merge_requests_enabled?
+ errors << "Merge requests are not enabled for project #{target_project.full_path}"
+ end
+
+ if branches.size > LIMIT
+ errors << "Too many branches pushed (#{branches.size} were pushed, limit is #{LIMIT})"
+ end
+
+ if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target])
+ errors << "Branch #{push_options[:target]} does not exist"
+ end
+ end
+
+ # Returns a Hash of branch => MergeRequest
+ def merge_requests
+ @merge_requests ||= MergeRequest.from_project(target_project)
+ .opened
+ .from_source_branches(branches)
+ .index_by(&:source_branch)
+ end
+
+ def execute_for_branch(branch)
+ merge_request = merge_requests[branch]
+
+ if merge_request
+ update!(merge_request)
+ else
+ create!(branch)
+ end
+ end
+
+ def create!(branch)
+ unless push_options[:create]
+ errors << "A merge_request.create push option is required to create a merge request for branch #{branch}"
+ return
+ end
+
+ # Use BuildService to assign the standard attributes of a merge request
+ merge_request = ::MergeRequests::BuildService.new(
+ project,
+ current_user,
+ create_params(branch)
+ ).execute
+
+ unless merge_request.errors.present?
+ merge_request = ::MergeRequests::CreateService.new(
+ project,
+ current_user,
+ merge_request.attributes
+ ).execute
+ end
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.persisted?
+ end
+
+ def update!(merge_request)
+ merge_request = ::MergeRequests::UpdateService.new(
+ target_project,
+ current_user,
+ update_params
+ ).execute(merge_request)
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.valid?
+ end
+
+ def create_params(branch)
+ params = {
+ assignee: current_user,
+ source_branch: branch,
+ source_project: project,
+ target_branch: push_options[:target] || target_project.default_branch,
+ target_project: target_project
+ }
+
+ if push_options.key?(:merge_when_pipeline_succeeds)
+ params.merge!(
+ merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
+ merge_user: current_user
+ )
+ end
+
+ params
+ end
+
+ def update_params
+ params = {}
+
+ if push_options.key?(:merge_when_pipeline_succeeds)
+ params.merge!(
+ merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
+ merge_user: current_user
+ )
+ end
+
+ if push_options.key?(:target)
+ params[:target_branch] = push_options[:target]
+ end
+
+ params
+ end
+
+ def collect_errors_from_merge_request(merge_request)
+ merge_request.errors.full_messages.each do |error|
+ errors << error
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 51d27673787..e0460f7081c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
post_merge_manually_merged
reload_merge_requests
outdate_suggestions
+ refresh_pipelines_on_merge_requests
reset_merge_when_pipeline_succeeds
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -107,8 +108,6 @@ module MergeRequests
end
merge_request.mark_as_unchecked
- create_pipeline_for(merge_request, current_user)
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
# Upcoming method calls need the refreshed version of
@@ -134,6 +133,13 @@ module MergeRequests
end
end
+ def refresh_pipelines_on_merge_requests
+ merge_requests_for_source_branch.each do |merge_request|
+ create_pipeline_for(merge_request, current_user)
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
def reset_merge_when_pipeline_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
end
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
index 81f6f92f75c..60a68568833 100644
--- a/app/services/note_summary.rb
+++ b/app/services/note_summary.rb
@@ -5,7 +5,9 @@ class NoteSummary
attr_reader :metadata
def initialize(noteable, project, author, body, action: nil, commit_count: nil)
- @note = { noteable: noteable, project: project, author: author, note: body }
+ @note = { noteable: noteable,
+ created_at: noteable.system_note_timestamp,
+ project: project, author: author, note: body }
@metadata = { action: action, commit_count: commit_count }.compact
set_commit_params if note[:noteable].is_a?(Commit)
diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
new file mode 100644
index 00000000000..f32a779fab0
--- /dev/null
+++ b/app/services/projects/update_statistics_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Projects
+ class UpdateStatisticsService < BaseService
+ def execute
+ return unless project && project.repository.exists?
+
+ Rails.logger.info("Updating statistics for project #{project.id}")
+
+ project.statistics.refresh!(only: statistics.map(&:to_sym))
+ end
+
+ private
+
+ def statistics
+ params[:statistics]
+ end
+ end
+end
diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb
index a04bb8f9e14..ff6b696ca96 100644
--- a/app/services/releases/concerns.rb
+++ b/app/services/releases/concerns.rb
@@ -15,7 +15,7 @@ module Releases
end
def name
- params[:name]
+ params[:name] || tag_name
end
def description
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index c6e143d440d..a271a7e5e49 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -15,6 +15,10 @@ module Releases
create_release(tag)
end
+ def find_or_build_release
+ release || build_release(existing_tag)
+ end
+
private
def ensure_tag
@@ -38,7 +42,17 @@ module Releases
end
def create_release(tag)
- release = project.releases.create!(
+ release = build_release(tag)
+
+ release.save!
+
+ success(tag: tag, release: release)
+ rescue => e
+ error(e.message, 400)
+ end
+
+ def build_release(tag)
+ project.releases.build(
name: name,
description: description,
author: current_user,
@@ -46,10 +60,6 @@ module Releases
sha: tag.dereferenced_target.sha,
links_attributes: params.dig(:assets, 'links') || []
)
-
- success(tag: tag, release: release)
- rescue => e
- error(e.message, 400)
end
end
end
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 039d6e2ebad..b45e567079b 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -12,7 +12,7 @@ module ResourceEvents
label_hash = {
resource_column(resource) => resource.id,
user_id: user.id,
- created_at: Time.now
+ created_at: resource.system_note_timestamp
}
labels = added_labels.map do |label|
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index ea8ac7e4656..acbbb0da929 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -258,7 +258,7 @@ module SystemNoteService
body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note = Note.create(note_attributes.merge(system: true))
+ note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp))
note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
note
diff --git a/app/views/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml
index 3319b4bad3a..13d10dcd625 100644
--- a/app/views/admin/users/_user_detail.html.haml
+++ b/app/views/admin/users/_user_detail.html.haml
@@ -6,7 +6,7 @@
= image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 prepend-top-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
= link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id }
- = render_if_exists 'admin/users/user_detail_note', user: user
+ = render_if_exists 'admin/users/user_listing_note', user: user
- user_badges_in_admin_section(user).each do |badge|
- css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present?
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 4ffa8d89504..c4178296e67 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -53,6 +53,8 @@
- else
Disabled
+ = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
+
%li
%span.light External User:
%strong
@@ -134,6 +136,8 @@
%strong
= link_to @user.created_by.name, [:admin, @user.created_by]
+ = render_if_exists partial: "namespaces/shared_runner_status", locals: { namespace: @user.namespace }
+
.col-md-6
- unless @user == current_user
- unless @user.confirmed?
@@ -146,6 +150,9 @@
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+
+ = render_if_exists 'admin/users/user_detail_note'
+
- if @user.blocked?
.card.border-info
.card-header.bg-info.text-white
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 3b6fc85e70e..369b0f7e62c 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,12 +6,12 @@
- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item d-flex', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
- else
- .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
+ .menu-item.mini-pipeline-graph-dropdown-item.d-flex{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9d069c025ba..6d48475d505 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -30,7 +30,7 @@
= custom_icon("icon_commit")
- if commit_sha
- = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha"
+ = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0"
- if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 282566eeadc..9774b797928 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -6,7 +6,7 @@
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
- = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha"
+ = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0"
%p.commit-title.flex-truncate-parent
%span.flex-truncate-child
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 85bc8ec07e3..a11e23b6daa 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -18,7 +18,7 @@
- if deployment.user
%div
by
- = user_avatar(user: deployment.user, size: 20)
+ = user_avatar(user: deployment.user, size: 20, css_class: "mr-0 float-none")
.table-section.section-15{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created")
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 3e8c2a1209a..f9b2e698fc9 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -143,6 +143,7 @@
- repository_remove_remote
- system_hook_push
- update_merge_requests
+- update_project_statistics
- upload_checksum
- web_hook
- repository_update_remote_mirror
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 396f44396a3..a5554f07699 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,7 +3,7 @@
class PostReceive
include ApplicationWorker
- def perform(gl_repository, identifier, changes, push_options = [])
+ def perform(gl_repository, identifier, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository)
if project.nil?
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b31099bc670..b2e0701008a 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -18,7 +18,7 @@ class ProjectCacheWorker
return unless project && project.repository.exists?
- update_statistics(project, statistics.map(&:to_sym))
+ update_statistics(project, statistics)
project.repository.refresh_method_caches(files.map(&:to_sym))
@@ -26,20 +26,28 @@ class ProjectCacheWorker
end
# rubocop: enable CodeReuse/ActiveRecord
+ # NOTE: triggering both an immediate update and one in 15 minutes if we
+ # successfully obtain the lease. That way, we only need to wait for the
+ # statistics to become accurate if they were already updated once in the
+ # last 15 minutes.
def update_statistics(project, statistics = [])
return if Gitlab::Database.read_only?
- return unless try_obtain_lease_for(project.id, :update_statistics)
+ return unless try_obtain_lease_for(project.id, statistics)
- Rails.logger.info("Updating statistics for project #{project.id}")
+ Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
- project.statistics.refresh!(only: statistics)
+ UpdateProjectStatisticsWorker.perform_in(LEASE_TIMEOUT, project.id, statistics)
end
private
- def try_obtain_lease_for(project_id, section)
+ def try_obtain_lease_for(project_id, statistics)
Gitlab::ExclusiveLease
- .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT)
+ .new(project_cache_worker_key(project_id, statistics), timeout: LEASE_TIMEOUT)
.try_obtain
end
+
+ def project_cache_worker_key(project_id, statistics)
+ ["project_cache_worker", project_id, *statistics.sort].join(":")
+ end
end
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
new file mode 100644
index 00000000000..9a29cc12707
--- /dev/null
+++ b/app/workers/update_project_statistics_worker.rb
@@ -0,0 +1,18 @@
+
+# frozen_string_literal: true
+
+# Worker for updating project statistics.
+class UpdateProjectStatisticsWorker
+ include ApplicationWorker
+
+ # project_id - The ID of the project for which to flush the cache.
+ # statistics - An Array containing columns from ProjectStatistics to
+ # refresh, if empty all columns will be refreshed
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(project_id, statistics = [])
+ project = Project.find_by(id: project_id)
+
+ Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/changelogs/unreleased/30157-api-expose-single-environment.yml b/changelogs/unreleased/30157-api-expose-single-environment.yml
new file mode 100644
index 00000000000..f9619dbcc7d
--- /dev/null
+++ b/changelogs/unreleased/30157-api-expose-single-environment.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add new API endpoint to expose a single environment.'
+merge_request: 26887
+author:
+type: added
diff --git a/changelogs/unreleased/43263-git-push-option-to-create-mr.yml b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
new file mode 100644
index 00000000000..d50c33da162
--- /dev/null
+++ b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests to be created via git push options
+merge_request: 26752
+author:
+type: added
diff --git a/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
new file mode 100644
index 00000000000..6fefd05049c
--- /dev/null
+++ b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
@@ -0,0 +1,6 @@
+---
+title: Allow merge requests to be set to merge when pipeline succeeds via git push
+ options
+merge_request: 26842
+author:
+type: added
diff --git a/changelogs/unreleased/53279-fix-updated_at-api.yml b/changelogs/unreleased/53279-fix-updated_at-api.yml
new file mode 100644
index 00000000000..c64dada7eaa
--- /dev/null
+++ b/changelogs/unreleased/53279-fix-updated_at-api.yml
@@ -0,0 +1,5 @@
+---
+title: "Respect updated_at attribute in notes produced by API calls"
+merge_request: 27124
+author: Ben Gamari
+type: fixed
diff --git a/changelogs/unreleased/59708-vendor-css.yml b/changelogs/unreleased/59708-vendor-css.yml
new file mode 100644
index 00000000000..ec7def7a9e6
--- /dev/null
+++ b/changelogs/unreleased/59708-vendor-css.yml
@@ -0,0 +1,5 @@
+---
+title: Creates a vendors folder for external CSS
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/delay-update-statictics.yml b/changelogs/unreleased/delay-update-statictics.yml
new file mode 100644
index 00000000000..d0201fb6db8
--- /dev/null
+++ b/changelogs/unreleased/delay-update-statictics.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the bug that the project statistics is not updated
+merge_request: 26854
+author: Hiroyuki Sato
+type: fixed
diff --git a/changelogs/unreleased/fix-pull-request-importer.yml b/changelogs/unreleased/fix-pull-request-importer.yml
new file mode 100644
index 00000000000..5f642a0710b
--- /dev/null
+++ b/changelogs/unreleased/fix-pull-request-importer.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of PR import
+merge_request: 27121
+author:
+type: performance
diff --git a/changelogs/unreleased/instance-configuration-artifact-size.yml b/changelogs/unreleased/instance-configuration-artifact-size.yml
new file mode 100644
index 00000000000..077f8631af5
--- /dev/null
+++ b/changelogs/unreleased/instance-configuration-artifact-size.yml
@@ -0,0 +1,5 @@
+---
+title: Display maximum artifact size from runtime config
+merge_request: 26784
+author: Bastian Blank
+type: fixed
diff --git a/changelogs/unreleased/issue-58418-release-notes.yml b/changelogs/unreleased/issue-58418-release-notes.yml
new file mode 100644
index 00000000000..80e6529eb12
--- /dev/null
+++ b/changelogs/unreleased/issue-58418-release-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Set release name when adding release notes to an existing tag
+merge_request: 26807
+author:
+type: fixed
diff --git a/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
new file mode 100644
index 00000000000..d003ca55feb
--- /dev/null
+++ b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
@@ -0,0 +1,5 @@
+---
+title: Create pipelines for merge requests only when source branch is updated
+merge_request: 26921
+author:
+type: fixed
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2dc0da00919..8bc2426ec4c 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -89,4 +89,5 @@
- [project_daily_statistics, 1]
- [import_issues_csv, 2]
- [chat_notification, 2]
- - [migrate_external_diffs, 1]
+ - [migrate_external_diffs, 1]
+ - [update_project_statistics, 1]
diff --git a/db/migrate/20180115094742_add_default_project_creation_setting.rb b/db/migrate/20180115094742_add_default_project_creation_setting.rb
new file mode 100644
index 00000000000..465a89c39e8
--- /dev/null
+++ b/db/migrate/20180115094742_add_default_project_creation_setting.rb
@@ -0,0 +1,19 @@
+class AddDefaultProjectCreationSetting < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:application_settings, :default_project_creation)
+ add_column_with_default(:application_settings, :default_project_creation, :integer, default: 2)
+ end
+ end
+
+ def down
+ if column_exists?(:application_settings, :default_project_creation)
+ remove_column(:application_settings, :default_project_creation)
+ end
+ end
+end
diff --git a/db/migrate/20180115113902_add_project_creation_level_to_groups.rb b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb
new file mode 100644
index 00000000000..a10ce54087c
--- /dev/null
+++ b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb
@@ -0,0 +1,17 @@
+class AddProjectCreationLevelToGroups < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:namespaces, :project_creation_level)
+ add_column(:namespaces, :project_creation_level, :integer)
+ end
+ end
+
+ def down
+ if column_exists?(:namespaces, :project_creation_level)
+ remove_column(:namespaces, :project_creation_level, :integer)
+ end
+ end
+end
diff --git a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb
deleted file mode 100644
index 87427ad2930..00000000000
--- a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class AddDefaultProjectCreationApplicationSetting < ActiveRecord::Migration[5.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- def up
- unless column_exists?(:application_settings, :default_project_creation)
- add_column(:application_settings, :default_project_creation, :integer, default: 2, null: false)
- end
- end
-
- def down
- if column_exists?(:application_settings, :default_project_creation)
- remove_column(:application_settings, :default_project_creation)
- end
- end
-end
diff --git a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb
deleted file mode 100644
index 159e0a95ace..00000000000
--- a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class AddProjectCreationLevelToNamespaces < ActiveRecord::Migration[5.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- def up
- unless column_exists?(:namespaces, :project_creation_level)
- add_column :namespaces, :project_creation_level, :integer
- end
- end
-
- def down
- unless column_exists?(:namespaces, :project_creation_level)
- remove_column :namespaces, :project_creation_level, :integer
- end
- end
-end
diff --git a/doc/README.md b/doc/README.md
index aead50ea97e..14a1eeffda0 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
-| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. |
+| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
index 638405126a5..aa4e1b0d2e0 100644
--- a/doc/administration/auth/okta.md
+++ b/doc/administration/auth/okta.md
@@ -92,18 +92,23 @@ Now that the Okta app is configured, it's time to enable it in GitLab.
1. Add the provider configuration.
>**Notes:**
+ >
>- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
installation to generate the correct value).
+ >
>- To get the `idp_cert_fingerprint` fingerprint, first download the
certificate from the Okta app you registered and then run:
`openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
with the location of your certificate.
+ >
>- Change the value of `idp_sso_target_url`, with the value of the
**Identity Provider Single Sign-On URL** from the step when you
configured the Okta app.
- >- Change the value of `issuer` to a unique name, which will identify the application
+ >
+ >- Change the value of `issuer` to the value of the **Audience Restriction** from your Okta app configuration. This will identify GitLab
to the IdP.
+ >
>- Leave `name_identifier_format` as-is.
**For Omnibus GitLab installations**
diff --git a/doc/api/commits.md b/doc/api/commits.md
index e205307eeca..92f53c7b5e6 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -130,6 +130,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Cont
```
Example response:
+
```json
{
"id": "ed899a2f4b50b4370feeea94676502b42383c746",
@@ -162,21 +163,21 @@ curl --request POST \
--form "branch=master" \
--form "commit_message=some commit message" \
--form "start_branch=master" \
- --form "actions[][action]=create" \
- --form "actions[][file_path]=foo/bar" \
- --form "actions[][content]=</path/to/local.file" \
- --form "actions[][action]=delete" \
- --form "actions[][file_path]=foo/bar2" \
- --form "actions[][action]=move" \
- --form "actions[][file_path]=foo/bar3" \
- --form "actions[][previous_path]=foo/bar4" \
- --form "actions[][content]=</path/to/local1.file" \
- --form "actions[][action]=update" \
+ --form "actions[][action]=create" \
+ --form "actions[][file_path]=foo/bar" \
+ --form "actions[][content]=</path/to/local.file" \
+ --form "actions[][action]=delete" \
+ --form "actions[][file_path]=foo/bar2" \
+ --form "actions[][action]=move" \
+ --form "actions[][file_path]=foo/bar3" \
+ --form "actions[][previous_path]=foo/bar4" \
+ --form "actions[][content]=</path/to/local1.file" \
+ --form "actions[][action]=update" \
--form "actions[][file_path]=foo/bar5" \
- --form "actions[][content]=</path/to/local2.file" \
- --form "actions[][action]=chmod" \
+ --form "actions[][content]=</path/to/local2.file" \
+ --form "actions[][action]=chmod" \
--form "actions[][file_path]=foo/bar5" \
- --form "actions[][execute_filemode]=true" \
+ --form "actions[][execute_filemode]=true" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/1/repository/commits"
```
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 4a38dd73747..ebcdc546d08 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -29,6 +29,111 @@ Example response:
]
```
+## Get a specific environment
+
+```
+GET /projects/:id/environments/:environment_id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `environment_id` | integer | yes | The ID of the environment |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1"
+```
+
+Example of response
+
+```json
+{
+ "id": 1,
+ "name": "review/fix-foo",
+ "slug": "review-fix-foo-dfjre3",
+ "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
+ "last_deployment": {
+ "id": 100,
+ "iid": 34,
+ "ref": "fdroid",
+ "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "created_at": "2019-03-25T18:55:13.252Z",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ }
+ "deployable": {
+ "id": 710,
+ "status": "success",
+ "stage": "deploy",
+ "name": "staging",
+ "ref": "fdroid",
+ "tag": false,
+ "coverage": null,
+ "created_at": "2019-03-25T18:55:13.215Z",
+ "started_at": "2019-03-25T12:54:50.082Z",
+ "finished_at": "2019-03-25T18:55:13.216Z",
+ "duration": 21623.13423,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.dev/root",
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "bio": null,
+ "location": null,
+ "public_email": "",
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": null
+ }
+ "commit": {
+ "id": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "short_id": "416d8ea1",
+ "created_at": "2016-01-02T15:39:18.000Z",
+ "parent_ids": [
+ "e9a4449c95c64358840902508fc827f1a2eab7df"
+ ],
+ "title": "Removed fabric to fix #40",
+ "message": "Removed fabric to fix #40\n",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "authored_date": "2016-01-02T15:39:18.000Z",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "committed_date": "2016-01-02T15:39:18.000Z"
+ },
+ "pipeline": {
+ "id": 34,
+ "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "ref": "fdroid",
+ "status": "success",
+ "web_url": "http://localhost:3000/Commit451/lab-coat/pipelines/34"
+ },
+ "web_url": "http://localhost:3000/Commit451/lab-coat/-/jobs/710",
+ "artifacts": [
+ {
+ "file_type": "trace",
+ "size": 1305,
+ "filename": "job.log",
+ "file_format": null
+ }
+ ],
+ "runner": null,
+ "artifacts_expire_at": null
+ }
+ }
+}
+```
+
## Create a new environment
Creates a new environment with the given name and external_url.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 913e9d4d789..a7ad2018b09 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -62,6 +62,7 @@ into more features:
| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes in a per-branch basis. |
+| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. |
| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md
index a06fe6961a7..5ecdf0f8c54 100644
--- a/doc/ci/chatops/README.md
+++ b/doc/ci/chatops/README.md
@@ -24,7 +24,7 @@ Since ChatOps is built upon GitLab CI/CD, the job has all the same features and
* It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline.
* If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started.
-* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](https://docs.gitlab.com/ce/ci/variables/README.html#priority-of-variables).
+* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](../variables/README.html#priority-of-environment-variables).
### Controlling the ChatOps reply
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 9266c4511be..5222cc45bc4 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -303,20 +303,19 @@ services:
- docker:dind
variables:
- CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
script:
- - docker pull $CONTAINER_IMAGE:latest || true
- - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_COMMIT_SHA --tag $CONTAINER_IMAGE:latest .
- - docker push $CONTAINER_IMAGE:$CI_COMMIT_SHA
- - docker push $CONTAINER_IMAGE:latest
+ - docker pull $CI_REGISTRY_IMAGE:latest || true
+ - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
+ - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
+ - docker push $CI_REGISTRY_IMAGE:latest
```
The steps in the `script` section for the `build` stage can be summed up to:
@@ -324,7 +323,7 @@ The steps in the `script` section for the `build` stage can be summed up to:
1. The first command tries to pull the image from the registry so that it can be
used as a cache for the `docker build` command.
1. The second command builds a Docker image using the pulled image as a
- cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if
+ cache (notice the `--cache-from $CI_REGISTRY_IMAGE:latest` argument) if
available, and tags it.
1. The last two commands push the tagged Docker images to the container registry
so that they may also be used as cache for subsequent builds.
@@ -421,14 +420,14 @@ and depend on the visibility of your project.
For all projects, mostly suitable for public ones:
-- **Using the special `gitlab-ci-token` user**: This user is created for you in order to
+- **Using the special `$CI_REGISTRY_USER` variable**: The user specified by this variable is created for you in order to
push to the Registry connected to your project. Its password is automatically
- set with the `$CI_JOB_TOKEN` variable. This allows you to automate building and deploying
+ set with the `$CI_REGISTRY_PASSWORD` variable. This allows you to automate building and deploying
your Docker images and has read/write access to the Registry. This is ephemeral,
so it's only valid for one job. You can use the following example as-is:
```sh
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
```
For private and internal projects:
@@ -436,8 +435,10 @@ For private and internal projects:
- **Using a personal access token**: You can create and use a
[personal access token](../../user/profile/personal_access_tokens.md)
in case your project is private:
- - For read (pull) access, the scope should be `read_registry`.
- - For read/write (pull/push) access, use `api`.
+
+ - For read (pull) access, the scope should be `read_registry`.
+ - For read/write (pull/push) access, use `api`.
+
Replace the `<username>` and `<access_token>` in the following example:
```sh
@@ -469,9 +470,9 @@ could look like:
DOCKER_DRIVER: overlay2
stage: build
script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- - docker build -t registry.example.com/group/project/image:latest .
- - docker push registry.example.com/group/project/image:latest
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - docker build -t $CI_REGISTRY/group/project/image:latest .
+ - docker push $CI_REGISTRY/group/project/image:latest
```
You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
@@ -486,7 +487,7 @@ variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
@@ -526,7 +527,7 @@ variables:
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 3314c76d234..13c26bc5f47 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -500,7 +500,7 @@ To configure access for `registry.example.com:5000`, follow these steps:
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
```
-1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the
+1. Create a [variable](../variables/README.md#gitlab-cicd-environment-variables) `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
```json
@@ -642,7 +642,6 @@ creation.
[postgres-hub]: https://hub.docker.com/r/_/postgres/
[mysql-hub]: https://hub.docker.com/r/_/mysql/
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
-[variable]: ../variables/README.md#variables
[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
[cmd]: https://docs.docker.com/engine/reference/builder/#cmd
[register]: https://docs.gitlab.com/runner/register/
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index eaafc7bc1c0..3e52cc786dd 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -224,7 +224,7 @@ The `name` and `url` parameters for dynamic environments can use most available
including:
- [Predefined environment variables](variables/README.md#predefined-environment-variables)
-- [Project and group variables](variables/README.md#variables)
+- [Project and group variables](variables/README.md#gitlab-cicd-environment-variables)
- [`.gitlab-ci.yml` variables](yaml/README.md#variables)
However, you cannot use variables defined:
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 0f809ed05ca..f56d5429fb7 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -116,7 +116,7 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa
```
-Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables).
+Now, let's add it to your GitLab project as a [variable](../../variables/README.md#gitlab-cicd-environment-variables).
Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**.
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index fa18cb22aed..e1164b8d03a 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -69,5 +69,5 @@ in the `.gitlab-ci.yml` file with your application's name.
## Heroku API key
You can look up your Heroku API key in your
-[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-variables) with
+[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-environment-variables) with
this value in **Project ➔ Variables** with key `HEROKU_API_KEY`.
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
new file mode 100644
index 00000000000..cfe638c0a22
--- /dev/null
+++ b/doc/ci/large_repositories/index.md
@@ -0,0 +1,235 @@
+# Optimizing GitLab for large repositories
+
+Large repositories consisting of more than 50k files in a worktree
+often require special consideration because of
+the time required to clone and check out.
+
+GitLab and GitLab Runner handle this scenario well
+but require optimized configuration to efficiently perform its
+set of operations.
+
+The general guidelines for handling big repositories are simple.
+Each guideline is described in more detail in the sections below:
+
+- Always fetch incrementally. Do not clone in a way that results in recreating all of the worktree.
+- Always use shallow clone to reduce data transfer. Be aware that this puts more burden
+ on GitLab instance due to higher CPU impact.
+- Control the clone directory if you heavily use a fork-based workflow.
+- Optimize `git clean` flags to ensure that you remove or keep data that might affect or speed-up your build.
+
+## Shallow cloning
+
+> Introduced in GitLab Runner 8.9.
+
+GitLab and GitLab Runner always perform a full clone by default.
+While it means that all changes from GitLab are received,
+it often results in receiving extra commit logs.
+
+Ideally, you should always use `GIT_DEPTH` with a small number
+like 10. This will instruct GitLab Runner to perform shallow clones.
+Shallow clones makes Git request only the latest set of changes for a given branch,
+up to desired number of commits as defined by the `GIT_DEPTH` variable.
+
+This significantly speeds up fetching of changes from Git repositories,
+especially if the repository has a very long backlog consisting of number
+of big files as we effectively reduce amount of data transfer.
+
+The following example makes GitLab Runner shallow clone to fetch only a given branch,
+it does not fetch any other branches nor tags.
+
+```yaml
+variables:
+ GIT_DEPTH: 10
+
+test:
+ script:
+ - ls -al
+```
+
+## Git strategy
+
+> Introduced in GitLab Runner 8.9.
+
+By default, GitLab is configured to always prefer the `GIT_STRATEGY: fetch` strategy.
+The `GIT_STRATEGY: fetch` strategy will re-use existing worktrees if found
+on disk. This is different to the `GIT_STRATEGY: clone` strategy
+as in case of clones, if a worktree is found, it is removed before clone.
+
+Usage of `fetch` is preferred because it reduces the amount of data to transfer and
+does not really impact the operations that you might do on a repository from CI.
+
+However, `fetch` does require access to the previous worktree. This works
+well when using the `shell` or `docker` executor because these
+try to preserve worktrees and try to re-use them by default.
+
+This does not work today for `kubernetes` executor and has limitations when using
+`docker+machine`. `kubernetes` executor today always clones into ephemeral directory.
+
+GitLab also offers the `GIT_STRATEGY: none` strategy. This disables any `fetch` and `checkout` commands
+done by GitLab, requiring you to do them.
+
+## Git clone path
+
+> Introduced in GitLab Runner 11.10.
+
+`GIT_CLONE_PATH` allows you to control where you clone your sources.
+This can have implications if you heavily use big repositories with fork workflow.
+
+Fork workflow from GitLab Runner's perspective is stored as a separate repository
+with separate worktree. That means that GitLab Runner cannot optimize the usage
+of worktrees and you might have to instruct GitLab Runner to use that.
+
+In such cases, ideally you want to make the GitLab Runner executor be used only used only
+for the given project and not shared across different projects to make this
+process more efficient.
+
+The `GIT_CLONE_PATH` has to be within the `$CI_BUILDS_DIR`. Currently,
+it is impossible to pick any path from disk.
+
+## Git clean flags
+
+> Introduced in GitLab Runner 11.10.
+
+`GIT_CLEAN_FLAGS` allows you to control whether or not you require
+the `git clean` command to be executed for each CI job.
+By default, GitLab ensures that you have your worktree on the given SHA,
+and that your repository is clean.
+
+`GIT_CLEAN_FLAGS` is disabled when set to `none`. On very big repositories, this
+might be desired because `git clean` is disk I/O intensive. Controlling that
+with `GIT_CLEAN_FLAGS: -ffdx -e .build/`, for example, allows you to control and
+disable removal of some directories within the worktree between subsequent runs,
+which can speed-up the incremental builds. This has the biggest effect
+if you re-use existing machines, and have an existing worktree that you can re-use
+for builds.
+
+For exact parameters accepted by `GIT_CLEAN_FLAGS`, see the documentation
+for [git clean](https://git-scm.com/docs/git-clean). The
+available parameters are dependent on Git version.
+
+## Fork-based workflow
+
+> Introduced in GitLab Runner 11.10.
+
+Following the guidelines above, lets imagine that we want to:
+
+- Optimize for a big project (more than 50k files in directory).
+- Use forks-based workflow for contributing.
+- Reuse existing worktrees. Have preconfigured runners that are pre-cloned with repositories.
+- Runner assigned only to project and all forks.
+
+Lets consider the following two examples, one using `shell` executor and
+other using `docker` executor.
+
+### `shell` executor example
+
+Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "shell"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ [runners.custom_build_dir]
+ enabled = true
+```
+
+This `config.toml`:
+
+- Uses the `shell` executor,
+- Specifies a custom `/builds` directory where all clones will be stored.
+- Enables the ability to specify `GIT_CLONE_PATH`,
+- Runs at most 4 jobs at once.
+
+### `docker` executor example
+
+Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "docker"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ [runners.docker]
+ volumes = ["/builds:/builds", "/cache:/cache"]
+```
+
+This `config.toml`:
+
+- Uses the `docker` executor,
+- Specifies a custom `/builds` directory on disk where all clones will be stored.
+ We host mount the `/builds` directory to make it reusable between subsequent runs
+ and be allowed to override the cloning strategy.
+- Doesn't enable the ability to specify `GIT_CLONE_PATH` as it is enabled by default.
+- Runs at most 4 jobs at once.
+
+### Our `.gitlab-ci.yml`
+
+Once we have the executor configured, we need to fine tune our `.gitlab-ci.yml`.
+
+Our pipeline will be most performant if we use the following `.gitlab-ci.yml`:
+
+```yaml
+variables:
+ GIT_DEPTH: 10
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME
+
+build:
+ script: ls -al
+```
+
+The above configures a:
+
+- Shallow clone of 10, to speed up subsequent `git fetch` commands.
+- Custom clone path to make it possible to re-use worktrees between parent project and all forks
+ because we use the same clone path for all forks.
+
+Why use `$CI_CONCURRENT_ID`? The main reason is to ensure that worktrees used are not conflicting
+between projects. The `$CI_CONCURRENT_ID` represents a unique identifier within the given executor,
+so as long as we use it to construct the path, it is guaranteed that this directory will not conflict
+with other concurrent jobs running.
+
+### Store custom clone options in `config.toml`
+
+Ideally, all job-related configuration should be stored in `.gitlab-ci.yml`.
+However, sometimes it is desirable to make these schemes part of Runner configuration.
+
+In the above example of Forks, making this configuration discoverable for users may be preferred,
+but this brings administrative overhead as the `.gitlab-ci.yml` needs to be updated for each branch.
+In such cases, it might be desirable to keep the `.gitlab-ci.yml` clone path agnostic, but make it
+a configuration of Runner.
+
+We can extend our [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
+with the following specification that will be used by Runner if `.gitlab-ci.yml` will not override it:
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "docker"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ environment = [
+ "GIT_DEPTH=10",
+ "GIT_CLONE_PATH=$CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME"
+ ]
+
+ [runners.docker]
+ volumes = ["/builds:/builds", "/cache:/cache"]
+```
+
+This makes the cloning configuration to be part of given Runner,
+and does not require us to update each `.gitlab-ci.yml`.
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index ff63d0bd69f..9ed1ec5aa5c 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy.
**Do not** add a passphrase to the SSH key, or the `before_script` will
prompt for it.
-1. Create a new [variable](../variables/README.md#variables).
+1. Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables).
As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste
the content of your _private_ key that you created earlier.
@@ -157,7 +157,7 @@ ssh-keyscan example.com
ssh-keyscan 1.2.3.4
```
-Create a new [variable](../variables/README.md#variables) with
+Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables) with
`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`.
NOTE: **Note:**
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 398b017277f..0e72bdddee7 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -20,7 +20,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new
DANGER: **Danger:**
Passing plain text tokens in public projects is a security issue. Potential
attackers can impersonate the user that exposed their trigger token publicly in
-their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#variables)
+their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#gitlab-cicd-environment-variables)
to protect trigger tokens.
## Adding a new trigger
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 592fdfd2873..61d1a904f76 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -3,7 +3,6 @@ table_display_block: true
---
# GitLab CI/CD environment variables
-{: #variables}
After a brief overview over the use of environment
variables, this document teaches you how to use GitLab CI/CD's
@@ -65,7 +64,7 @@ To get started, choose one of the existing
to be output by the Runner. For example, let's say that you want
a given job you're running through your script to output the
stage that job is running for. In your `.gitlab-ci.yml` file,
-call the variable from your script according to the [syntaxes](#syntax-of-variables-in-job-scripts) available. To
+call the variable from your script according to the [syntaxes](#syntax-of-environment-variables-in-job-scripts) available. To
output the job stage, use the predefined variable `CI_JOB_STAGE`:
```yaml
@@ -145,7 +144,6 @@ settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
Follow the discussion in issue [#13784][ce-13784] for masking the variables.
### Syntax of environment variables in job scripts
-{: #syntax-of-variables-in-job-scripts}
All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
@@ -278,7 +276,6 @@ script:
```
### Group-level environment variables
-{: #group-level-variables}
> Introduced in GitLab 9.4.
@@ -297,19 +294,18 @@ Any variables of [subgroups](../../user/group/subgroups/index.md) will be inheri
Once you set them, they will be available for all subsequent pipelines.
## Priority of environment variables
-{: #priority-of-variables}
Variables of different types can take precedence over other
variables, depending on where they are defined.
The order of precedence for variables is (from highest to lowest):
-1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables).
-1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-variables).
-1. Group-level [variables](#group-level-variables) or [protected variables](#protected-variables).
+1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#using-variables).
+1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-environment-variables).
+1. Group-level [variables](#group-level-environment-variables) or [protected variables](#protected-environment-variables).
1. YAML-defined [job-level variables](../yaml/README.md#variables).
1. YAML-defined [global variables](../yaml/README.md#variables).
-1. [Deployment variables](#deployment-variables).
+1. [Deployment variables](#deployment-environment-variables).
1. [Predefined environment variables](predefined_variables.md).
For example, if you define:
@@ -329,7 +325,6 @@ about which variables are [not supported](where_variables_can_be_used.md).
## Advanced use
### Protected environment variables
-{: #protected-variables}
> Introduced in GitLab 9.3.
@@ -345,7 +340,6 @@ Protected variables can be added by going to your project's
Once you set them, they will be available for all subsequent pipelines.
### Deployment environment variables
-{: #deployment-variables}
> Introduced in GitLab 8.15.
@@ -382,7 +376,7 @@ limitations with the current Auto DevOps scripting environment.
[Manually triggered pipelines](../pipelines.md#manually-executing-pipelines) allow you to override the value of a current variable.
For instance, suppose you added a
-[custom variable `$TEST`](#creating-a-custom-variable)
+[custom variable `$TEST`](#creating-a-custom-environment-variable)
as exemplified above and you want to override it in a manual pipeline.
Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**.
Choose the branch you want to run the pipeline for, then add a new variable
@@ -396,7 +390,6 @@ value you set for this specific pipeline:
![Manually overridden variable output](img/override_value_via_manual_pipeline_output.png)
## Environment variables expressions
-{: #variables-expressions}
> Introduced in GitLab 10.7.
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index b429dc8c8be..846c539daab 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -36,7 +36,7 @@ future GitLab releases.**
| `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message |
| `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
-| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](README.md#debug-tracing) is enabled |
| `CI_DEPLOY_PASSWORD` | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| `CI_DEPLOY_USER` | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| `CI_DISPOSABLE_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 1218ac0b071..091ddcb0bae 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -111,4 +111,4 @@ They are:
- Script execution shell.
- Not supported:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- - In the `only` and `except` [variables expressions](README.md#variables-expressions).
+ - In the `only` and `except` [variables expressions](README.md#environment-variables-expressions).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 1d76f911943..5e44de13b51 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -518,7 +518,7 @@ end-to-end:
- $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
```
-Learn more about [variables expressions](../variables/README.md#variables-expressions).
+Learn more about [variables expressions](../variables/README.md#environment-variables-expressions).
#### `only:changes`/`except:changes`
@@ -2249,7 +2249,7 @@ Runner itself](../variables/README.md#predefined-environment-variables).
One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called
-[Variables](../variables/README.md#variables)
+[Variables](../variables/README.md#gitlab-cicd-environment-variables)
which can be set in GitLab's UI.
Learn more about [variables and their priority][variables].
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index a4da34a50ce..4a2ff64807e 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -48,29 +48,21 @@ as its markdown rendering engine. See the [GitLab Markdown Guide](https://about.
Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is missing, you are welcome to suggest one via a merge request.
-## Documentation directory structure
+## Documentation types and organization
The documentation is structured based on the GitLab UI structure itself,
separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
-In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
-all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
-
-The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
-been **deprecated** and the majority their docs have been moved to their correct location
-in small iterations. Please do not create new docs in these folders. Organize docs by product area and subject, not type.
+Organize docs by product area and subject, not type. For example, do not create groupings of similar media types
+(e.g. indexes of all articles, videos, etc.).
-### Documentation files
-
-- When you create a new directory, always start with an `index.md` file.
- Do not use another file name and **do not** create `README.md` files.
-- **Do not** use special chars and spaces, or capital letters in file names,
- directory names, branch names, and anything that generates a path.
-- Max screenshot size: 100KB.
-- We do not support videos (yet).
+Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes
+it difficult to browse for the information you need and difficult to maintain up-to-date content.
+Instead, organize content by its subject (e.g. everything related to CI goes together)
+and cross-link between any related content.
-### Location and naming documents
+### Location and naming of files
Our goal is to have a clear hierarchical structure with meaningful URLs
like `docs.gitlab.com/user/project/merge_requests/`. With this pattern,
@@ -78,16 +70,8 @@ you can immediately tell that you are navigating to user-related documentation
about project features; specifically about merge requests. Our site's paths match
those of our repository, so the clear structure also makes documentation easier to update.
-While the documentation is home to a variety of content types, we do not organize by content type.
-For example, do not create groupings of similar media types (e.g. indexes of all articles, videos, etc.).
-Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes
-it difficult to browse for the information you need and difficult to maintain up-to-date content.
-Instead, organize content by its subject (e.g. everything related to CI goes together)
-and cross-link between any related content.
-
-Do not simply link out to GitLab technical blog posts. There should be an up-to-date
-single source of truth on the topic within the documentation, and the top of the
-blog post should be updated to link to that doc.
+In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
+all docs should be linked at least from its higher-level index page if not also from other relevant locations.
The table below shows what kind of documentation goes where.
@@ -102,13 +86,18 @@ The table below shows what kind of documentation goes where.
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
-**General rules & best practices:**
+**Rules and best practices:**
+1. When you create a new directory, always start with an `index.md` file.
+ Do not use another file name and **do not** create `README.md` files.
+1. **Do not** use special chars and spaces, or capital letters in file names,
+ directory names, branch names, and anything that generates a path.
1. When creating a new document and it has more than one word in its name,
make sure to use underscores instead of spaces or dashes (`-`). For example,
a proper naming would be `import_projects_from_github.md`. The same rule
applies to images.
-1. Start a new directory with an `index.md` file.
+1. For image files, do not exceed 100KB.
+1. We do not yet support embedded videos. Please link out.
1. There are four main directories, `user`, `administration`, `api` and `development`.
1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
`profile/`, `dashboard/` and `admin_area/`.
@@ -129,6 +118,9 @@ The table below shows what kind of documentation goes where.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
General user- and admin- related documentation, should be placed accordingly.
+1. The directories `/workflow/`, `/university/`, and `/articles/` have
+been **deprecated** and the majority their docs have been moved to their correct location
+in small iterations.
If you are unsure where a document or a content addition should live, this should
not stop you from authoring and contributing. You can use your best judgment and
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c0386290785..1cefe48b4c1 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -14,9 +14,8 @@ For programmatic help adhering to the guidelines, see [linting](index.md#linting
## Files
-- [Directory structure](index.md#location-and-naming-documents): place the docs
- in the correct location.
-- [Documentation files](index.md#documentation-files): name the files accordingly.
+See the [Documentation types and organization](index.md#documentation-types-and-organization) section
+on the index page for information on how to structure and name files across the GitLab documentation.
DANGER: **Attention:**
**Do not** use capital letters, spaces, or special chars in file names,
@@ -28,41 +27,43 @@ a test that will fail if it spots a new `README.md` file.
### Markdown
-The [documentation website](https://docs.gitlab.com) had its markdown engine migrated from [Redcarpet to GitLab Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108)
-in October 2018.
+The [documentation website](https://docs.gitlab.com) uses GitLab Kramdown as its Markdown rendering engine [as of October 2018](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108). For a complete Kramdown reference, see the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown)
gem will support all [GFM markup](../../user/markdown.md) in the future. For now,
-use regular markdown markup, following the rules on this style guide. For a complete
-Kramdown reference, check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
-Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in
-[`/help`](index.md#gitlab-help).
+use regular markdown markup, following the rules in the linked style guide.
+
+Note that Kramdown-specific markup (e.g., `{:.class}`) will not render properly in on GitLab instances in [`/help`](index.md#gitlab-help).
## Content
These guidelines help toward the goal of having every user's search of documentation
yield a useful result, and ensuring content is helpful and easy to consume.
-- What to include:
- - Any and all helpful information, processes, and tips for implementing,
- using, and troubleshooting GitLab features. [The documentation is the single source of truth](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot)
- for this information.
- - 'Risky' or niche problem-solving steps. There is no reason to withhold these or
- store them elsewhere; simply include them along with the rest of the docs including all necessary
- detail, such as specific warnings and caveats about potential ramifications.
- - Any content types/sources, if relevant to users or admins. You can freely
- include presentations, videos, etc.; no matter who it was originally written for,
- if it is helpful to any of our audiences, we can include it. If an outside source
- that's under copyright, rephrase, or summarize and link out; do not copy and paste.
- - All applicable subsections as described on the [structure and template](structure.md) page,
- with files organized in the [correct directory](index.md#documentation-directory-structure).
+### Subject matter
+
+[The documentation is the single source of truth (SSOT)](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) for any and all helpful information and processes needed to learn about, implement, use, and troubleshoot GitLab features. Note that this includes problem-solving actions that may address rare cases or be considered 'risky', so long as proper context is provided. See the SSOT link for more detail.
+
+### Media types and sources
+
+Include any media types/sources, if relevant to readers. You can freely include or link presentations, diagrams, videos, etc.; no matter who it was originally composed for, if it is helpful to any of our audiences, we can include it.
+
+ - If you use an image that has a separate source file (for example, a vector or diagram format), link the image to the source file so that it may be reused or updated by anyone.
+ - Do not copy and paste content from other sources unless it is a limited quotation with the source cited. Typically it is better to either rephrase relevant information in your own words or link out to the other source.
+
+### Structure across documents
+
+- Place files in the correct directory per the [Documentation directory structure](index.md#documentation-types-and-organization) guidelines.
+- To avoid duplication, do not include the same information in multiple places. Instead, choose one 'single source of truth (SSOT)' location and link from other relevant locations.
+- When referencing other GitLab products and features, link to their respective docs.
+- When referencing third-party products or technologies, link out to their external sites, documentation, and resources.
+
+### Structure within documents
+
+- Include any and all applicable subsections as described on the [structure and template](structure.md) page,
- To ensure discoverability, link to each doc from its higher-level index page and other related pages.
-- When referencing other GitLab products and features, link to their
- respective docs; when referencing third-party products or technologies,
- link out to their external sites, documentation, and resources.
-- Do not duplicate information.
- Structure content in alphabetical order in tables, lists, etc., unless there is
- a logical reason not to (for example, when mirroring the UI or an ordered sequence).
+ a logical reason not to (for example, when mirroring the UI or an otherwise ordered sequence).
## Language
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 9452593c510..c5a1d915be6 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -19,6 +19,11 @@ CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
+You can force Webpack to act as CE by either deleting the `ee/` directory or by
+setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/helpers/is_ee_env.js)
+to something that evaluates as `false`. The same works for running tests
+(for example `IS_GITLAB_EE=0 yarn jest`).
+
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500
## Separation of EE code
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index a34d1fcec89..437ce9abc7d 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -38,7 +38,7 @@ _For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-### A `index.js` file
+### An `index.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
@@ -46,7 +46,7 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Don't forget to follow [these steps][page_specific_javascript].
+Be sure to read about [page-specific JavaScript][page_specific_javascript].
### Bootstrapping Gotchas
#### Providing data from HAML to JavaScript
@@ -240,7 +240,7 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
-[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
+[page_specific_javascript]: ./performance.md#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 7b54fa6289c..7d52cac5f7e 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -83,7 +83,7 @@ In this file, we will write the actions that will call the respective mutations:
export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
- export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error);
+ export const receiveUsersError = ({ commit }, error) => commit(types.RECEIVE_USERS_ERROR, error);
export const fetchUsers = ({ state, dispatch }) => {
dispatch('requestUsers');
@@ -175,7 +175,7 @@ Remember that actions only describe that something happened, they don't describe
state.users = data;
state.isLoading = false;
},
- [types.REQUEST_USERS_ERROR](state, error) {
+ [types.RECEIVE_USERS_ERROR](state, error) {
state.isLoading = false;
},
[types.REQUEST_ADD_USER](state, user) {
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index 27b69ba8278..f8cf2a7fdc6 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -192,7 +192,7 @@ GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thom
To use a custom Gitaly repository in CI, for instance if you want your
GitLab fork to always use your own Gitaly fork, set `GITALY_REPO_URL`
-as a [CI environment variable](../ci/variables/README.md#variables).
+as a [CI environment variable](../ci/variables/README.md#gitlab-cicd-environment-variables).
---
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 7262c04d746..e41148360f2 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -216,8 +216,10 @@ project, one project will do for the entire file. This can be achieved by using
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
-There is one gotcha; you can't reference a model defined in a `let` block in a
-`set` block.
+Note that you can't reference a model defined in a `let` block in a `set` block.
+
+Also, `set` is not supported in `:js` specs since those don't use transactions
+to clean up database state after each example.
### Time-sensitive tests
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 818a03a6f02..6e2a09fc030 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../user/project/issues/index.md#new-issue'
+redirect_to: '../user/project/issues/index.md#issue-actions'
---
-This document was moved to [another location](../user/project/issues/index.md#new-issue).
+This document was moved to [another location](../user/project/issues/index.md#issue-actions).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 574ba961cb0..32c2e4a5c99 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -632,7 +632,7 @@ directory (repositories, uploads).
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for Omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key,
-[CI/CD variables](../ci/variables/README.md#variables), and
+[CI/CD variables](../ci/variables/README.md#gitlab-cicd-environment-variables), and
variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 9060360e6a2..1df492ba82c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -141,7 +141,7 @@ in any of the following places:
NOTE: **Note**
The Auto DevOps base domain variable (`KUBE_INGRESS_BASE_DOMAIN`) follows the same order of precedence
-as other environment [variables](../../ci/variables/README.md#priority-of-variables).
+as other environment [variables](../../ci/variables/README.md#priority-of-environment-variables).
A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
@@ -669,7 +669,7 @@ repo or by specifying a project variable:
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
-- **Project variable** - Create a [project variable](../../ci/variables/README.md#variables)
+- **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart.
### Custom Helm chart per environment **[PREMIUM]**
@@ -770,7 +770,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
TIP: **Tip:**
Set up the replica variables using a
-[project variable](../../ci/variables/README.md#variables)
+[project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
and scale your application by just redeploying it!
CAUTION: **Caution:**
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 9c3f6fcec9b..74735886350 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -58,7 +58,7 @@ By doing so:
## Issues and merge requests within a group
Issues and merge requests are part of projects. For a given group, view all the
-[issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
+[issues](../project/issues/index.md#issues-list) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
together in a single list view.
## Create a new group
diff --git a/doc/user/index.md b/doc/user/index.md
index d408504249e..626246447f3 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -38,8 +38,8 @@ GitLab is a Git-based platform that integrates a great number of essential tools
- Hosting code in repositories with version control.
- Tracking proposals for new implementations, bug reports, and feedback with a
- fully featured [Issue Tracker](project/issues/index.md#issue-tracker).
-- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-board).
+ fully featured [Issue Tracker](project/issues/index.md#issues-list).
+- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-boards).
- Reviewing code in [Merge Requests](project/merge_requests/index.md) with live-preview changes per
branch with [Review Apps](../ci/review_apps/index.md).
- Building, testing, and deploying with built-in [Continuous Integration](../ci/README.md).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 8239742969a..9891a43aa61 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -578,11 +578,11 @@ Alt-H2
------
```
-### Header IDs and links
+#### Header IDs and links
-All Markdown-rendered headers automatically get IDs, except in comments.
+All Markdown-rendered headers automatically get IDs, which can be linked to, except in comments.
-On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
+On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to use it somewhere else.
The IDs are generated from the content of the header according to the following rules:
@@ -609,8 +609,8 @@ Would generate the following link IDs:
1. `this-header-has-spaces-in-it`
1. `this-header-has-a-in-it`
1. `this-header-has-unicode-in-it-한글`
-1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1`
+1. `this-header-has-spaces-in-it-2`
1. `this-header-has-3-5-in-it-and-parentheses`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
@@ -715,25 +715,25 @@ Becomes:
There are two ways to create links, inline-style and reference-style.
- [I'm an inline-style link](https://www.google.com)
-
- [I'm a reference-style link][Arbitrary case-insensitive reference text]
-
- [I'm a relative reference to a repository file](LICENSE)
-
- [I am an absolute reference within the repository](/doc/user/markdown.md)
-
- [I link to the Milestones page](/../milestones)
+```markdown
+[I'm an inline-style link](https://www.google.com)
+[I'm a link to a repository file in the same directory](index.md)
+[I am an absolute reference within the repository](/doc/user/index.md)
+[I'm a relative link to the Milestones page](../README.md)
- [You can use numbers for reference-style link definitions][1]
+[I link to a section on a different markdown page, using a header ID](index.md#overview)
+[I link to a different section on the same page, using the header ID](#header-ids-and-links)
- Or leave it empty and use the [link text itself][]
+[I'm a reference-style link][Arbitrary case-insensitive reference text]
+[You can use numbers for reference-style link definitions][1]
+Or leave it empty and use the [link text itself][]
- Some text to show that the reference links can follow later.
+Some text to show that the reference links can follow later.
- [arbitrary case-insensitive reference text]: https://www.mozilla.org
- [1]: http://slashdot.org
- [link text itself]: https://www.reddit.com
+[arbitrary case-insensitive reference text]: https://www.mozilla.org
+[1]: http://slashdot.org
+[link text itself]: https://www.reddit.com
+```
>**Note:**
Relative links do not allow referencing project files in a wiki page or wiki
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 878d30dddaa..141fe488357 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -314,7 +314,7 @@ install it manually.
## Installing applications
-GitLab provides a one-click install for various applications which can
+GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
be added directly to your configured cluster. Those applications are
needed for [Review Apps](../../../ci/review_apps/index.md) and
[deployments](../../../ci/environments.md). You can install them after you
@@ -545,7 +545,7 @@ The result will then be:
## Deployment variables
The Kubernetes cluster integration exposes the following
-[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+[deployment variables](../../../ci/variables/README.md#deployment-environment-variables) in the
GitLab CI/CD build environment.
| Variable | Description |
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index ca19ce4d328..ad47b848bea 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -42,7 +42,7 @@ below.
## How it works
The Issue Board builds on GitLab's existing
-[issue tracking functionality](issues/index.md#issue-tracker) and
+[issue tracking functionality](issues/index.md#issues-list) and
leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board.
With the Issue Board you can have a different view of your issues while
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index 40040e44d64..9a147deecd4 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -7,7 +7,7 @@ the information illustrated on the image below.
![New issue from the issues list](img/new_issue.png)
-Read through the [issues functionalities documentation](issues_functionalities.md#issues-functionalities)
+Read through the [issue data and actions documentation](issue_data_and_actions.md#parts-of-an-issue)
to understand these fields one by one.
## New issue from the Issue Tracker
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 675c280a12a..c82b7f100d2 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -1,170 +1,135 @@
# Issues
-The GitLab Issue Tracker is an advanced and complete tool
-for tracking the evolution of a new idea or the process
-of solving a problem.
+Issues are the fundamental medium for collaborating on ideas and planning work in GitLab.
-It allows you, your team, and your collaborators to share
-and discuss proposals before and while implementing them.
+## Overview
-GitLab Issues and the GitLab Issue Tracker are available in all
-[GitLab Products](https://about.gitlab.com/pricing/) as
-part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems, and planning work.
-## Use cases
+Issues can allow you, your team, and your collaborators to share and discuss proposals before and during their implementation.
+However, they can be used for a variety of other purposes, customized to your needs and workflow.
-Issues can have endless applications. Just to exemplify, these are
-some cases for which creating issues are most used:
+Issues are always associated with a specific project, but if you have multiple projects in a group,
+you can also view all the issues collectively at the group level.
+
+**Common use cases include:**
- Discussing the implementation of a new idea
-- Submitting feature proposals
-- Asking questions
-- Reporting bugs and malfunction
-- Obtaining support
+- Tracking tasks and work status
+- Accepting feature proposals, questions, support requests, or bug reports
- Elaborating new code implementations
See also the blog post "[Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)".
-### Keep private things private
-
-For instance, let's assume you have a public project but want to start a discussion on something
-you don't want to be public. With [Confidential Issues](#confidential-issues),
-you can discuss private matters among the project members, and still keep
-your project public, open to collaboration.
-
-### Streamline collaboration
-
-With [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
-available in [GitLab Starter](https://about.gitlab.com/pricing/)
-you can streamline collaboration and allow shared responsibilities to be clearly displayed.
-All assignees are shown across your workflows and receive notifications (as they
-would as single assignees), simplifying communication and ownership.
-
-### Consistent collaboration
-
-Create [issue templates](#issue-templates) to make collaboration consistent and
-containing all information you need. For example, you can create a template
-for feature proposals and another one for bug reports.
-
-## Issue Tracker
-
-The Issue Tracker is the collection of opened and closed issues created in a project.
-It is available for all projects, from the moment the project is created.
-
-Find the issue tracker by navigating to your **Project's homepage** > **Issues**.
-
-### Issues per project
-
-When you access your project's issues, GitLab will present them in a list,
-and you can use the tabs available to quickly filter by open and closed issues.
-
-![Project issues list view](img/project_issues_list_view.png)
-
-You can also [search and filter](../../search/index.md#issues-and-merge-requests-per-project) the results more deeply with GitLab's search capacities.
-
-### Issues per group
-
-View issues in all projects in the group, including all projects of all descendant subgroups of the group. Navigate to **Group > Issues** to view these issues. This view also has the open and closed issues tabs.
-
-![Group Issues list view](img/group_issues_list_view.png)
-
-## GitLab Issues Functionalities
-
-The image bellow illustrates how an issue looks like:
+## Parts of an issue
+
+Issues contain a variety of content and metadata, enabling a large range of flexibility in how they are used. Each issue can contain the following attributes, though some items may remain unset.
+
+<table class="borderless-table fixed-table">
+<tr>
+ <td>
+ <ul>
+ <li>Content</li>
+ <ul>
+ <li>Title</li>
+ <li>Description and tasks</li>
+ <li>Comments and other activity</li>
+ </ul>
+ <li>People</li>
+ <ul>
+ <li>Author</li>
+ <li>Assignee(s)</li>
+ </ul>
+ <li>State</li>
+ <ul>
+ <li>Status (open/closed)</li>
+ <li>Confidentiality</li>
+ <li>Tasks (completed vs. outstanding)</li>
+ </ul>
+ </ul>
+ </td>
+ <td>
+ <ul>
+ <li>Planning and tracking</li>
+ <ul>
+ <li>Milestone</li>
+ <li>Due date</li>
+ <li>Weight</li>
+ <li>Time tracking</li>
+ <li>Labels</li>
+ <li>Votes</li>
+ <li>Reaction emoji</li>
+ <li>Linked issues</li>
+ <li>Assigned epic</li>
+ <li>Unique issue number and URL</li>
+ </ul>
+ </ul>
+ </td>
+</tr>
+</table>
+
+## Viewing and managing issues
+
+While you can view and manage the full detail of an issue at its URL, you can also work with multiple issues at a time using the Issues List, Issue Boards, Epics **[ULTIMATE]**, and issue references.
+
+### Issue page
![Issue view](img/issues_main_view.png)
-Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
-
-## New issue
+On an issue’s page, you can view all aspects of the issue, and you can also modify them if you you have the necessary [permissions](../../permissions.md).
-Read through the [documentation on creating issues](create_new_issue.md).
+For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page.
-## Closing issues
+### Issues list
-Learn distinct ways to [close issues](closing_issues.md) in GitLab.
-
-## Moving issues
-
-Read through the [documentation on moving issues](moving_issues.md).
-
-## Deleting issues
+![Project issues list view](img/project_issues_list_view.png)
-Read through the [documentation on deleting issues](deleting_issues.md)
+On the Issues List, you can view all issues in the current project, or from multiple projects when opening the Issues List from the higher-level group context. Filter the issue list by [any search query](../../search/index.md#issues-and-merge-requests-per-project) and/or specific metadata, such as label(s), assignees(s), status, and more. From this view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues.
-## Create a merge request from an issue
+For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page.
-Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request).
+### Issue boards
-## Search for an issue
+![Issue board](img/issue_board.png)
-Learn how to [find an issue](../../search/index.md) by searching for and filtering them.
+Issue boards are Kanban boards with columns that display issues based on their labels or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using highly customizable workflows.
-## Advanced features
+You can reorder issues within a column, or drag an issue card to another column; its associated label or assignee will change to match that of the new column. The entire board can also be filtered to only include issues from a certain milestone or an overarching label.
-### Confidential Issues
+For more information, see the [Issue Boards](../issue_board.md) page.
-Whenever you want to keep the discussion presented in a
-issue within your team only, you can make that
-[issue confidential](confidential_issues.md). Even if your project
-is public, that issue will be preserved. The browser will
-respond with a 404 error whenever someone who is not a project
-member with at least [Reporter level](../../permissions.md#project-members-permissions) tries to
-access that issue's URL.
+### Epics **[ULTIMATE]**
-Learn more about them on the [confidential issues documentation](confidential_issues.md).
+Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones.
-### Issue templates
+For more information, see the [Epics](https://docs.gitlab.com/ee/user/group/epics/) page.
-Create templates for every new issue. They will be available from
-the dropdown menu **Choose a template** when you create a new issue:
+### Related issues **[STARTER]**
-![issue template](img/issue_template.png)
+You can mark two issues as related, so that when viewing each one, the other is always listed in its Related Issues section. This can help display important context, such as past work, dependencies, or duplicates.
-Learn more about them on the [issue templates documentation](../../project/description_templates.md#creating-issue-templates).
+For more information, see [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html).
### Crosslinking issues
-Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
-
-### Issue Board
-
-The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
-enhance your workflow by organizing and prioritizing issues in GitLab.
-
-![Issue board](img/issue_board.png)
-
-Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issues** > **Board**.
-
-Read through the documentation for [Issue Boards](../issue_board.md)
-to find out more about this feature.
-
-With [GitLab Starter](https://about.gitlab.com/pricing/), you can also
-create various boards per project with [Multiple Issue Boards](../issue_board.html#multiple-issue-boards-starter).
-
-### Import Issues from CSV
-
-You can import a CSV file containing issue titles and descriptions to create
-a batch of issues simultaneously.
-
-When you navigate to the Issues list page, an import button is displayed.
-
-For further details, see [Importing issues from CSV](csv_import.md)
-
-### External Issue Tracker
-
-Alternatively to GitLab's built-in Issue Tracker, you can also use an [external
-tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
-YouTrack, or Bugzilla.
-
-### Issue API
+When you reference an issue from another issue or merge request by including its URL or ID, the referenced issue displays a message in the Activity stream about the reference, with a link to the other issue or MR.
-See the [API documentation](../../../api/issues.md).
+For more information, see [Crosslinking issues](crosslinking_issues.md).
-### Bulk editing issues
+## Issue actions
-See the [bulk editing issues](../../project/bulk_editing.md) page.
+- [Create an issue](create_new_issue.md)
+- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
+- [Close an issue](closing_issues.md)
+- [Move an issue](moving_issues.md)
+- [Delete an issue](deleting_issues.md)
+- [Create a merge request from an issue](issue_data_and_actions.md#18-new-merge-request)
-### Similar issues
+## Advanced issue management
-See the [similar issues](similar_issues.md) page.
+- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues in order to change their status, assignee, milestone, or labels in bulk.
+- [Import issues](csv_import.md)
+- [Export issues](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[STARTER]**
+- [Issues API](../../../api/issues.md)
+- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
+or Bugzilla.
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issue_data_and_actions.md
index 27b9dc51760..653bd94e513 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -1,8 +1,8 @@
-# GitLab Issues Functionalities
+# Issue Data and Actions
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-## Issues Functionalities
+## Parts of an Issue
The image below illustrates what an issue looks like:
@@ -77,7 +77,7 @@ can be changed as many times as needed.
Categorize issues by giving them [labels](../labels.md). They help to
organize workflows, and they enable you to work with the
-[GitLab Issue Board](index.md#issue-board).
+[GitLab Issue Board](index.md#issue-boards).
Group Labels, which allow you to use the same labels for a
group of projects, can be also given to issues. They work exactly the same,
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 01a3a5bbbe1..ba7d05a7ad7 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -219,6 +219,64 @@ apply the patches. The target branch can be specified using the
[`/target_branch` quick action](../quick_actions.md). If the source
branch already exists, the patches will be applied on top of it.
+## Git push options
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26752) in GitLab 11.10.
+
+NOTE: **Note:**
+Git push options are only available with Git 2.10 or newer.
+
+GitLab supports using
+[Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
+to perform the following actions against merge requests at the same time
+as pushing changes:
+
+- Create a new merge request for the pushed branch.
+- Set the target of the merge request to a particular branch.
+- Set the merge request to merge when its pipeline succeeds.
+
+### Create a new merge request using git push options
+
+To create a new merge request for a branch, use the
+`merge_request.create` push option:
+
+```sh
+git push -o merge_request.create
+```
+
+### Set the target branch of a merge request using git push options
+
+To update an existing merge request's target branch, use the
+`merge_request.target=<branch_name>` push option:
+
+```sh
+git push -o merge_request.target=branch_name
+```
+
+You can also create a merge request and set its target branch at the
+same time using a `-o` flag per push option:
+
+```sh
+git push -o merge_request.create -o merge_request.target=branch_name
+```
+
+### Set merge when pipeline succeeds using git push options
+
+To set an existing merge request to
+[merge when its pipeline succeeds](merge_when_pipeline_succeeds.md), use
+the `merge_request.merge_when_pipeline_succeeds` push option:
+
+```sh
+git push -o merge_request.merge_when_pipeline_succeeds
+```
+
+You can also create a merge request and set it to merge when its
+pipeline succeeds at the same time using a `-o` flag per push option:
+
+```sh
+git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds
+```
+
## Find the merge request that introduced a change
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
@@ -254,7 +312,7 @@ To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
-[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
+[Learn more about setting a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
## Merge request diff file navigation
diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
index f639188684b..5ad500c4d20 100644
--- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
+++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
@@ -134,7 +134,7 @@ Now that your certificate has been issued, let's add it to your Pages site:
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem | pbcopy
```
-1. Copy and paste the public key into the second field **Key (PEM)**:
+1. Copy and paste the private key into the second field **Key (PEM)**:
```bash
sudo cat /etc/letsencrypt/live/example.com/privkey.pem | pbcopy
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 4a989afad4d..8b762307ac4 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -182,4 +182,4 @@ https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?st
## Environment Variables
-[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner.
+[Environment variables](../../../ci/variables/README.html#gitlab-cicd-environment-variables) can be set in an environment to be available to a runner.
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2dd3120d3fc..f9773086f69 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1294,10 +1294,6 @@ module API
expose :id, :name, :slug, :external_url
end
- class Environment < EnvironmentBasic
- expose :project, using: Entities::BasicProjectDetails
- end
-
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
@@ -1305,6 +1301,11 @@ module API
expose :deployable, using: Entities::Job
end
+ class Environment < EnvironmentBasic
+ expose :project, using: Entities::BasicProjectDetails
+ expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
+ end
+
class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
expose :url, as: :html_url
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 5b0f3b914cb..6cd43923559 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -101,6 +101,21 @@ module API
status 200
present environment, with: Entities::Environment, current_user: current_user
end
+
+ desc 'Get a single environment' do
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ get ':id/environments/:environment_id' do
+ authorize! :read_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+ present environment, with: Entities::Environment, current_user: current_user,
+ except: [:project, { last_deployment: [:environment] }],
+ last_deployment: true
+ end
end
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 3fd824877ae..71c30ec99a5 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -43,6 +43,28 @@ module API
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
+ def process_mr_push_options(push_options, project, user, changes)
+ output = {}
+
+ service = ::MergeRequests::PushOptionsHandlerService.new(
+ project,
+ user,
+ changes,
+ push_options
+ ).execute
+
+ if service.errors.present?
+ output[:warnings] = push_options_warning(service.errors.join("\n\n"))
+ end
+
+ output
+ end
+
+ def push_options_warning(warning)
+ options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ')
+ "Error encountered with push options #{options}: #{warning}"
+ end
+
def redis_ping
result = Gitlab::Redis::SharedState.with { |redis| redis.ping }
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 9c7b9146c8f..00f0bbab231 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -256,19 +256,27 @@ module API
post '/post_receive' do
status 200
+ output = {} # Messages to gitlab-shell
+ user = identify(params[:identifier])
+ project = Gitlab::GlRepository.parse(params[:gl_repository]).first
+ push_options = Gitlab::PushOptions.new(params[:push_options])
+
PostReceive.perform_async(params[:gl_repository], params[:identifier],
- params[:changes], params[:push_options].to_a)
+ params[:changes], push_options.as_json)
+
+ if Feature.enabled?(:mr_push_options, default_enabled: true)
+ mr_options = push_options.get(:merge_request)
+ output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present?
+ end
+
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
- output = {
- merge_request_urls: merge_request_urls,
+ output.merge!(
broadcast_message: broadcast_message,
- reference_counter_decreased: reference_counter_decreased
- }
-
- project = Gitlab::GlRepository.parse(params[:gl_repository]).first
- user = identify(params[:identifier])
+ reference_counter_decreased: reference_counter_decreased,
+ merge_request_urls: merge_request_urls
+ )
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 999a9cb5a82..000c00ea9f9 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -230,9 +230,13 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
- # Setting created_at time only allowed for admins and project/group owners
- unless current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner)
- params.delete(:updated_at)
+ # Setting updated_at only allowed for admins and owners as well
+ if params[:updated_at].present?
+ if current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner)
+ issue.system_note_timestamp = params[:updated_at]
+ else
+ params.delete(:updated_at)
+ end
end
update_params = declared_params(include_missing: false).merge(request: request, api: true)
diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb
index 5251e0fadf9..2e3a4f3b869 100644
--- a/lib/gitlab/background_migration.rb
+++ b/lib/gitlab/background_migration.rb
@@ -58,11 +58,31 @@ module Gitlab
migration_class_for(class_name).new.perform(*arguments)
end
- def self.exists?(migration_class)
+ def self.exists?(migration_class, additional_queues = [])
enqueued = Sidekiq::Queue.new(self.queue)
scheduled = Sidekiq::ScheduledSet.new
- [enqueued, scheduled].each do |queue|
+ enqueued_job?([enqueued, scheduled], migration_class)
+ end
+
+ def self.dead_jobs?(migration_class)
+ dead_set = Sidekiq::DeadSet.new
+
+ enqueued_job?([dead_set], migration_class)
+ end
+
+ def self.retrying_jobs?(migration_class)
+ retry_set = Sidekiq::RetrySet.new
+
+ enqueued_job?([retry_set], migration_class)
+ end
+
+ def self.migration_class_for(class_name)
+ const_get(class_name)
+ end
+
+ def self.enqueued_job?(queues, migration_class)
+ queues.each do |queue|
queue.each do |job|
return true if job.queue == self.queue && job.args.first == migration_class
end
@@ -70,9 +90,5 @@ module Gitlab
false
end
-
- def self.migration_class_for(class_name)
- const_get(class_name)
- end
end
end
diff --git a/lib/gitlab/background_migration/migrate_stage_index.rb b/lib/gitlab/background_migration/migrate_stage_index.rb
index f90f35a913d..f921233460d 100644
--- a/lib/gitlab/background_migration/migrate_stage_index.rb
+++ b/lib/gitlab/background_migration/migrate_stage_index.rb
@@ -21,8 +21,8 @@ module Gitlab
AND stage_idx IS NOT NULL
GROUP BY stage_id, stage_idx
), indexes AS (
- SELECT DISTINCT stage_id, last_value(stage_idx)
- OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
+ SELECT DISTINCT stage_id, first_value(stage_idx)
+ OVER (PARTITION BY stage_id ORDER BY freq DESC) AS index
FROM freqs
)
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 69a3a1aedef..5a13fd18504 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -36,10 +36,12 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :url,
+ type: String,
length: { maximum: 255 },
allow_nil: true
validates :action,
+ type: String,
inclusion: { in: %w[start stop], message: 'should be start or stop' },
allow_nil: true
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 79bbcc1ed1e..7d6e0704d4a 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -8,7 +8,6 @@ module Gitlab
include ::Gitlab::Utils::StrongMemoize
SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
- SKIP_PUSH_OPTION = 'ci.skip'
def perform!
if skipped?
@@ -35,7 +34,8 @@ module Gitlab
end
def push_option_skips_ci?
- !!(@command.push_options&.include?(SKIP_PUSH_OPTION))
+ @command.push_options.present? &&
+ @command.push_options.deep_symbolize_keys.dig(:ci, :skip).present?
end
end
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index ea08b5f7eae..af385d7d4ca 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -32,10 +32,7 @@ module Gitlab
}
],
total_commits_count: 1,
- push_options: [
- "ci.skip",
- "custom option"
- ]
+ push_options: { ci: { skip: true } }
}.freeze
# Produce a hash of post-receive data
@@ -57,11 +54,11 @@ module Gitlab
# },
# commits: Array,
# total_commits_count: Fixnum,
- # push_options: Array
+ # push_options: Hash
# }
#
# rubocop:disable Metrics/ParameterLists
- def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: [])
+ def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {})
commits = Array(commits)
# Total commits count
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index 426436c2164..d98b85fecc4 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -5,7 +5,7 @@ module Gitlab
include Gitlab::Identifier
attr_reader :project, :identifier, :changes, :push_options
- def initialize(project, identifier, changes, push_options)
+ def initialize(project, identifier, changes, push_options = {})
@project = project
@identifier = identifier
@changes = deserialize_changes(changes)
diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb
index b3fe1fc0685..4bc39868389 100644
--- a/lib/gitlab/import/merge_request_helpers.rb
+++ b/lib/gitlab/import/merge_request_helpers.rb
@@ -22,7 +22,7 @@ module Gitlab
# additional work that is strictly necessary.
merge_request_id = insert_and_return_id(attributes, project.merge_requests)
- merge_request = project.merge_requests.reload.find(merge_request_id)
+ merge_request = project.merge_requests.reset.find(merge_request_id)
[merge_request, false]
end
diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
index 8c0c17780ca..746786b5a66 100644
--- a/lib/gitlab/legacy_github_import/release_formatter.rb
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -7,6 +7,7 @@ module Gitlab
{
project: project,
tag: raw_data.tag_name,
+ name: raw_data.name,
description: raw_data.body,
created_at: raw_data.created_at,
updated_at: raw_data.created_at
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
new file mode 100644
index 00000000000..810aba436cc
--- /dev/null
+++ b/lib/gitlab/push_options.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class PushOptions
+ VALID_OPTIONS = HashWithIndifferentAccess.new({
+ merge_request: {
+ keys: [:create, :merge_when_pipeline_succeeds, :target]
+ },
+ ci: {
+ keys: [:skip]
+ }
+ }).freeze
+
+ NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
+ mr: :merge_request
+ }).freeze
+
+ OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/
+
+ attr_reader :options
+
+ def initialize(options = [])
+ @options = parse_options(options)
+ end
+
+ def get(*args)
+ options.dig(*args)
+ end
+
+ # Allow #to_json serialization
+ def as_json(*_args)
+ options
+ end
+
+ private
+
+ def parse_options(raw_options)
+ options = HashWithIndifferentAccess.new
+
+ Array.wrap(raw_options).each do |option|
+ namespace, key, value = parse_option(option)
+
+ next if [namespace, key].any?(&:nil?)
+
+ options[namespace] ||= HashWithIndifferentAccess.new
+ options[namespace][key] = value
+ end
+
+ options
+ end
+
+ def parse_option(option)
+ parts = OPTION_MATCHER.match(option)
+ return unless parts
+
+ namespace, key, value = parts.values_at(:namespace, :key, :value).map(&:strip)
+ namespace = NAMESPACE_ALIASES[namespace] if NAMESPACE_ALIASES[namespace]
+ value = value.presence || true
+
+ return unless valid_option?(namespace, key)
+
+ [namespace, key, value]
+ end
+
+ def valid_option?(namespace, key)
+ keys = VALID_OPTIONS.dig(namespace, :keys)
+ keys && keys.include?(key.to_sym)
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f22b351ad78..aa116a44254 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16,6 +16,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+msgid " Please sign in."
+msgstr ""
+
msgid " Status"
msgstr ""
@@ -408,6 +411,9 @@ msgstr ""
msgid "Access Tokens"
msgstr ""
+msgid "Access denied for your LDAP account."
+msgstr ""
+
msgid "Access denied! Please verify you can add deploy keys to this repository."
msgstr ""
@@ -1008,12 +1014,18 @@ msgstr ""
msgid "Authentication Log"
msgstr ""
+msgid "Authentication failed: %{error_message}"
+msgstr ""
+
msgid "Authentication log"
msgstr ""
msgid "Authentication method"
msgstr ""
+msgid "Authentication method updated"
+msgstr ""
+
msgid "Authentication via U2F device failed."
msgstr ""
@@ -1032,6 +1044,9 @@ msgstr ""
msgid "Authorize %{link_to_client} to use your account?"
msgstr ""
+msgid "Authorized %{new_chat_name}"
+msgstr ""
+
msgid "Authorized At"
msgstr ""
@@ -1467,6 +1482,12 @@ msgstr ""
msgid "Cannot be merged automatically"
msgstr ""
+msgid "Cannot create the abuse report. The user has been deleted."
+msgstr ""
+
+msgid "Cannot create the abuse report. This user has been blocked."
+msgstr ""
+
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
@@ -2525,12 +2546,18 @@ msgstr ""
msgid "Copy token to clipboard"
msgstr ""
+msgid "Could not authorize chat nickname. Try again!"
+msgstr ""
+
msgid "Could not connect to FogBugz, check your URL"
msgstr ""
msgid "Could not create Wiki Repository at this time. Please try again later."
msgstr ""
+msgid "Could not delete chat nickname %{chat_name}."
+msgstr ""
+
msgid "Could not remove the trigger."
msgstr ""
@@ -2540,6 +2567,9 @@ msgstr ""
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
+msgid "Could not revoke personal access token %{personal_access_token_name}."
+msgstr ""
+
msgid "Coverage"
msgstr ""
@@ -2552,6 +2582,9 @@ msgstr ""
msgid "Create New Domain"
msgstr ""
+msgid "Create a GitLab account first, and then connect it to your %{label} account."
+msgstr ""
+
msgid "Create a new branch"
msgstr ""
@@ -2804,6 +2837,12 @@ msgstr ""
msgid "Deleted"
msgstr ""
+msgid "Deleted chat nickname: %{chat_name}!"
+msgstr ""
+
+msgid "Denied authorization of chat nickname %{user_name}."
+msgstr ""
+
msgid "Deny"
msgstr ""
@@ -3643,6 +3682,12 @@ msgstr ""
msgid "Failed to save new settings"
msgstr ""
+msgid "Failed to save preferences (%{error_message})."
+msgstr ""
+
+msgid "Failed to save preferences."
+msgstr ""
+
msgid "Failed to update issues, please try again."
msgstr ""
@@ -5800,6 +5845,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Password authentication is unavailable."
+msgstr ""
+
msgid "Past due"
msgstr ""
@@ -6067,12 +6115,18 @@ msgstr ""
msgid "Please choose a group URL with no special characters."
msgstr ""
+msgid "Please complete your profile with email address"
+msgstr ""
+
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
msgstr ""
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr ""
+msgid "Please create a password for your new account."
+msgstr ""
+
msgid "Please create a username with only alphanumeric characters."
msgstr ""
@@ -6106,6 +6160,9 @@ msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Preferences saved."
+msgstr ""
+
msgid "Preferences|Navigation theme"
msgstr ""
@@ -6298,6 +6355,9 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
+msgid "Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
+msgstr ""
+
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
@@ -6992,6 +7052,9 @@ msgstr ""
msgid "Revoked impersonation token %{token_name}!"
msgstr ""
+msgid "Revoked personal access token %{personal_access_token_name}!"
+msgstr ""
+
msgid "Run untagged jobs"
msgstr ""
@@ -7480,6 +7543,12 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
+msgid "Signing in using %{label} has been disabled"
+msgstr ""
+
+msgid "Signing in using your %{label} account without a pre-existing GitLab account is not allowed."
+msgstr ""
+
msgid "Similar issues"
msgstr ""
@@ -7882,6 +7951,9 @@ msgstr ""
msgid "Successfully confirmed"
msgstr ""
+msgid "Successfully deleted U2F device."
+msgstr ""
+
msgid "Successfully removed email."
msgstr ""
@@ -8047,6 +8119,9 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Thank you for your report. A GitLab administrator will look into it shortly."
+msgstr ""
+
msgid "The %{type} contains the following error:"
msgid_plural "The %{type} contains the following errors:"
msgstr[0] ""
@@ -8109,6 +8184,12 @@ msgstr ""
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
+msgid "The invitation could not be accepted."
+msgstr ""
+
+msgid "The invitation could not be declined."
+msgstr ""
+
msgid "The invitation has already been accepted."
msgstr ""
@@ -9465,6 +9546,9 @@ msgstr ""
msgid "Write milestone description..."
msgstr ""
+msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
+msgstr ""
+
msgid "Yes"
msgstr ""
@@ -9591,6 +9675,15 @@ msgstr ""
msgid "You don't have any deployments right now."
msgstr ""
+msgid "You have been granted %{member_human_access} access to %{label}."
+msgstr ""
+
+msgid "You have been unsubscribed from this thread."
+msgstr ""
+
+msgid "You have declined the invitation to join %{label}."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -9747,6 +9840,12 @@ msgstr ""
msgid "Your name"
msgstr ""
+msgid "Your new personal access token has been created."
+msgstr ""
+
+msgid "Your password reset token has expired."
+msgstr ""
+
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr ""
diff --git a/package.json b/package.json
index 6d3e6764a46..115f6fd1e8d 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
"prettier-all-save": "node ./scripts/frontend/prettier.js save-all",
- "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* --custom-formatter node_modules/stylelint-error-string-formatter",
+ "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* ee/app/assets/stylesheets/**/*.* !**/vendors/** --custom-formatter node_modules/stylelint-error-string-formatter",
"stylelint-file": "node node_modules/stylelint/bin/stylelint.js",
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "yarn jest && yarn karma",
@@ -33,7 +33,7 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
- "@gitlab/svgs": "^1.58.0",
+ "@gitlab/svgs": "^1.59.0",
"@gitlab/ui": "^3.2.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index bb1f775da75..0971e551db1 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -40,7 +40,7 @@ module QA
Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE'
- resource.value = 'You can see this application secret'
+ resource.value = 'you_can_see_this_variable'
end
# Connect K8s cluster
@@ -99,7 +99,7 @@ module QA
Page::Project::Operations::Environments::Show.perform do |show|
show.view_deployment do
expect(page).to have_content('Hello World!')
- expect(page).to have_content('You can see this application secret')
+ expect(page).to have_content('you_can_see_this_variable')
end
end
end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index be13c3fb683..a235fddabca 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -34,7 +34,7 @@ RSpec.configure do |config|
config.display_try_failure_messages = true
config.around do |example|
- retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 3
+ retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2
example.run_with_retry retry: retry_times
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 43639875265..168c0168bba 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -419,6 +419,17 @@ describe Projects::EnvironmentsController do
expect(json_response['data']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+
+ context 'when time params are provided' do
+ it 'returns a metrics JSON document' do
+ additional_metrics(start: '1554702993.5398998', end: '1554717396.996232')
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['data']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ end
end
context 'when only one time param is provided' do
diff --git a/spec/controllers/projects/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb
index 29f206c574b..66eff4844c2 100644
--- a/spec/controllers/projects/tags/releases_controller_spec.rb
+++ b/spec/controllers/projects/tags/releases_controller_spec.rb
@@ -18,40 +18,85 @@ describe Projects::Tags::ReleasesController do
tag_id = release.tag
project.releases.destroy_all # rubocop: disable DestroyAll
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).not_to be_persisted
+ expect(response).to have_http_status(:ok)
end
it 'retrieves an existing release' do
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).to be_persisted
+ expect(response).to have_http_status(:ok)
end
end
describe 'PUT #update' do
it 'updates release note description' do
- update_release('description updated')
+ response = update_release(release.tag, "description updated")
- release = project.releases.find_by_tag(tag)
+ release = project.releases.find_by(tag: tag)
expect(release.description).to eq("description updated")
+ expect(response).to have_http_status(:found)
end
- it 'deletes release note when description is null' do
- expect { update_release('') }.to change(project.releases, :count).by(-1)
+ it 'creates a release if one does not exist' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "a new release")
+ end.to change { project.releases.count }.by(1)
+
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'sets the release name, sha, and author for a new release' do
+ tag_without_release = create_new_tag
+
+ response = update_release(tag_without_release.name, "a new release")
+
+ release = project.releases.find_by(tag: tag_without_release.name)
+ expect(release.name).to eq(tag_without_release.name)
+ expect(release.sha).to eq(tag_without_release.target_commit.sha)
+ expect(release.author.id).to eq(user.id)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'deletes release when description is empty' do
+ initial_releases_count = project.releases.count
+
+ response = update_release(release.tag, "")
+
+ expect(initial_releases_count).to eq(1)
+ expect(project.releases.count).to eq(0)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'does nothing when description is empty and the tag does not have a release' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "")
+ end.not_to change { project.releases.count }
+
+ expect(response).to have_http_status(:found)
end
end
- def update_release(description)
+ def create_new_tag
+ project.repository.add_tag(user, 'mytag', 'master')
+ end
+
+ def update_release(tag_id, description)
put :update, params: {
namespace_id: project.namespace.to_param,
project_id: project,
- tag_id: release.tag,
+ tag_id: tag_id,
release: { description: description }
}
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index dfdb8d589eb..b358c6b9c34 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', :js do
include BoardHelpers
+ include FilteredSearchHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -129,6 +130,7 @@ describe 'Issue Boards', :js do
click_link 'Unassigned'
end
+ close_dropdown_menu_if_visible
wait_for_requests
expect(page).to have_content('No assignee')
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index a0ae6720a9f..a19101366a0 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -13,7 +13,7 @@ describe 'Blob shortcuts', :js do
end
shared_examples "quotes the selected text" do
- it "quotes the selected text" do
+ it "quotes the selected text", :quarantine do
select_element('.note-text')
find('body').native.send_key('r')
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 7584339ccc0..7a6f76cb382 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -139,7 +139,7 @@ describe 'Dropdown milestone', :js do
expect_filtered_search_input_empty
end
- it 'fills in the milestone name when the milestone is partially filled' do
+ it 'fills in the milestone name when the milestone is partially filled', :quarantine do
filtered_search.send_keys('v')
click_milestone(milestone.title)
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index ea474759547..68af8303c2f 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -60,34 +60,7 @@ describe 'Issues > User uses quick actions', :js do
it_behaves_like 'remove_due_date quick action'
it_behaves_like 'duplicate quick action'
it_behaves_like 'create_merge_request quick action'
-
- describe 'adding a due date from note' do
- let(:issue) { create(:issue, project: project) }
-
- it_behaves_like 'due quick action available and date can be added'
-
- context 'when the current user cannot update the due date' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it_behaves_like 'due quick action not available'
- end
- end
-
- describe 'toggling the WIP prefix from the title from note' do
- let(:issue) { create(:issue, project: project) }
-
- it 'does not recognize the command nor create a note' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- end
- end
+ it_behaves_like 'due quick action'
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index bac297de4a6..489651fea15 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -21,7 +21,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do
end
shared_examples 'assigning labels from sidebar' do
- it 'can assign all ancestors labels' do
+ it 'can assign all ancestors labels', :quarantine do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
page.within('.block.labels') do
find('.edit-link').click
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index c837a6752f9..65de0afae0c 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -113,7 +113,7 @@ describe 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes', :quarantine do
indicator = find('.js-image-badge', match: :first)
badge = find('.user-avatar-link .badge', match: :first)
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 9909bfb5904..1b3718968b9 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -63,7 +63,7 @@ describe 'User visits the profile preferences page' do
end
describe 'User changes their language', :js do
- it 'creates a flash message' do
+ it 'creates a flash message', :quarantine do
select2('en', from: '#user_preferred_language')
click_button 'Save'
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index efb7b01f5ad..bcbba6f14da 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -43,7 +43,7 @@ describe "User creates wiki page" do
expect(page).to have_content("Create Page")
end
- it "shows non-escaped link in the pages list", :js do
+ it "shows non-escaped link in the pages list", :js, :quarantine do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql
new file mode 100644
index 00000000000..7b712068fcd
--- /dev/null
+++ b/spec/fixtures/api/graphql/introspection.graphql
@@ -0,0 +1,92 @@
+# pulled from GraphiQL query
+query IntrospectionQuery {
+ __schema {
+ queryType { name }
+ mutationType { name }
+ subscriptionType { name }
+ types {
+ ...FullType
+ }
+ directives {
+ name
+ description
+ locations
+ args {
+ ...InputValue
+ }
+ }
+ }
+}
+
+fragment FullType on __Type {
+ kind
+ name
+ description
+ fields(includeDeprecated: true) {
+ name
+ description
+ args {
+ ...InputValue
+ }
+ type {
+ ...TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ...InputValue
+ }
+ interfaces {
+ ...TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ description
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ...TypeRef
+ }
+}
+
+fragment InputValue on __InputValue {
+ name
+ description
+ type { ...TypeRef }
+ defaultValue
+}
+
+fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact.json b/spec/fixtures/api/schemas/public_api/v4/artifact.json
new file mode 100644
index 00000000000..9df957b1498
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": [
+ "file_type",
+ "size",
+ "filename",
+ "file_format"
+ ],
+ "properties": {
+ "file_type": { "type": "string"},
+ "size": { "type": "integer"},
+ "filename": { "type": "string"},
+ "file_format": { "type": "string"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact_file.json b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
new file mode 100644
index 00000000000..4017e6bdabc
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": [
+ "filename",
+ "size"
+ ],
+ "properties": {
+ "filename": { "type": "string"},
+ "size": { "type": "integer"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json
new file mode 100644
index 00000000000..3af2dc27d55
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json
@@ -0,0 +1,32 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "iid",
+ "ref",
+ "sha",
+ "created_at",
+ "user",
+ "deployable"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "ref": { "type": "string" },
+ "sha": { "type": "string" },
+ "created_at": { "type": "string" },
+ "user": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user/basic.json" }
+ ]
+ },
+ "deployable": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "job.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json
new file mode 100644
index 00000000000..242e90fb7ac
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/environment.json
@@ -0,0 +1,23 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "slug",
+ "external_url",
+ "last_deployment"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "slug": { "type": "string" },
+ "external_url": { "$ref": "../../types/nullable_string.json" },
+ "last_deployment": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "deployment.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
new file mode 100644
index 00000000000..454935422a0
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -0,0 +1,63 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "status",
+ "stage",
+ "name",
+ "ref",
+ "tag",
+ "coverage",
+ "created_at",
+ "started_at",
+ "finished_at",
+ "duration",
+ "user",
+ "commit",
+ "pipeline",
+ "web_url",
+ "artifacts",
+ "artifacts_expire_at",
+ "runner"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "status": { "type": "string" },
+ "stage": { "type": "string" },
+ "name": { "type": "string" },
+ "ref": { "type": "string" },
+ "tag": { "type": "boolean" },
+ "coverage": { "type": ["number", "null"] },
+ "created_at": { "type": "string" },
+ "started_at": { "type": ["null", "string"] },
+ "finished_at": { "type": ["null", "string"] },
+ "duration": { "type": ["null", "number"] },
+ "user": { "$ref": "user/basic.json" },
+ "commit": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "commit/basic.json" }
+ ]
+ },
+ "pipeline": { "$ref": "pipeline/basic.json" },
+ "web_url": { "type": "string" },
+ "artifacts": {
+ "type": "array",
+ "items": { "$ref": "artifact.json" }
+ },
+ "artifacts_file": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "artifact_file.json" }
+ ]
+ },
+ "artifacts_expire_at": { "type": ["null", "string"] },
+ "runner": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "runner.json" }
+ ]
+ }
+ },
+ "additionalProperties":false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/runner.json b/spec/fixtures/api/schemas/public_api/v4/runner.json
new file mode 100644
index 00000000000..d97d74a93f2
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/runner.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "description",
+ "ip_address",
+ "active",
+ "is_shared",
+ "name",
+ "online",
+ "status"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "description": { "type": "string" },
+ "ip_address": { "type": "string" },
+ "active": { "type": "boolean" },
+ "is_shared": { "type": "boolean" },
+ "name": { "type": ["null", "string"] },
+ "online": { "type": "boolean" },
+ "status": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 054dc27cda6..63004b96ad6 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -12,3 +12,6 @@ globals:
loadFixtures: false
preloadFixtures: false
setFixtures: false
+rules:
+ jest/no-focused-tests: error
+ jest/no-jasmine-globals: error
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 503af3920a8..67e5dc399ac 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -31,7 +31,7 @@ describe('ErrorTrackingList', () => {
actions = {
getErrorList: () => {},
startPolling: () => {},
- restartPolling: jasmine.createSpy('restartPolling'),
+ restartPolling: jest.fn().mockName('restartPolling'),
};
const state = {
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
index ea7c146fa4f..0e62bc94517 100644
--- a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
@@ -8,7 +8,7 @@ describe('RecentSearchesServiceError', () => {
});
it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
- expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
+ expect(recentSearchesServiceError).toEqual(expect.any(RecentSearchesServiceError));
expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
});
diff --git a/spec/frontend/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js
index af12ca15369..8596642eb7a 100644
--- a/spec/frontend/ide/lib/common/disposable_spec.js
+++ b/spec/frontend/ide/lib/common/disposable_spec.js
@@ -8,7 +8,7 @@ describe('Multi-file editor library disposable class', () => {
instance = new Disposable();
disposableClass = {
- dispose: jasmine.createSpy('dispose'),
+ dispose: jest.fn().mockName('dispose'),
};
});
diff --git a/spec/frontend/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js
index d149a883166..b07a583b7c8 100644
--- a/spec/frontend/ide/lib/editor_options_spec.js
+++ b/spec/frontend/ide/lib/editor_options_spec.js
@@ -2,7 +2,7 @@ import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
- expect(editorOptions).toEqual(jasmine.any(Array));
+ expect(editorOptions).toEqual(expect.any(Array));
});
it('contains readOnly option', () => {
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 1e0bc708c31..7e9aec84016 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -25,14 +25,14 @@ describe('Abuse Reports', () => {
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('originalMessage')).toEqual(jasmine.anything());
+ expect($longMessage.data('originalMessage')).toEqual(expect.anything());
assertMaxLength($longMessage);
});
it('should not truncate short messages', () => {
const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything());
+ expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', () => {
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index efc338b36eb..6ac1e83829f 100644
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -13,7 +13,7 @@ describe('EmojiMenu', () => {
let dummyEmojiList;
beforeEach(() => {
- dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback');
+ dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback');
dummyEmojiList = {
glEmojiTag() {
return dummyEmojiTag;
@@ -75,19 +75,19 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'one',
- jasmine.anything(),
+ expect.anything(),
'mouseenter focus',
dummyToggleButtonSelector,
'mouseenter focus',
- jasmine.anything(),
+ expect.anything(),
);
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
dummyToggleButtonSelector,
- jasmine.anything(),
+ expect.anything(),
);
});
@@ -96,10 +96,10 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
`.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
- jasmine.anything(),
+ expect.anything(),
);
});
});
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 5533de1a70a..7af33ceaadc 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -4,14 +4,21 @@ import axios from '~/lib/utils/axios_utils';
import functionsComponent from '~/serverless/components/functions.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { createStore } from '~/serverless/store';
+import { TEST_HOST } from 'helpers/test_constants';
import { mockServerlessFunctions } from '../mock_data';
describe('functionsComponent', () => {
+ const statusPath = `${TEST_HOST}/statusPath`;
+
let component;
let store;
let localVue;
+ let axiosMock;
beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(statusPath).reply(200);
+
localVue = createLocalVue();
localVue.use(Vuex);
@@ -20,6 +27,7 @@ describe('functionsComponent', () => {
afterEach(() => {
component.vm.$destroy();
+ axiosMock.restore();
});
it('should render empty state when Knative is not installed', () => {
@@ -80,11 +88,7 @@ describe('functionsComponent', () => {
);
});
- fit('should render the functions list', () => {
- const statusPath = 'statusPath';
- const axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(statusPath).reply(200);
-
+ it('should render the functions list', () => {
component = shallowMount(functionsComponent, {
localVue,
store,
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
new file mode 100644
index 00000000000..e3c455d1686
--- /dev/null
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -0,0 +1,29 @@
+import { getTimeDiff } from '~/monitoring/utils';
+import { timeWindows } from '~/monitoring/constants';
+
+describe('getTimeDiff', () => {
+ it('defaults to an 8 hour (28800s) difference', () => {
+ const params = getTimeDiff();
+
+ expect(params.end - params.start).toEqual(28800);
+ });
+
+ it('accepts time window as an argument', () => {
+ const params = getTimeDiff(timeWindows.thirtyMinutes);
+
+ expect(params.end - params.start).not.toEqual(28800);
+ });
+
+ it('returns a value for every defined time window', () => {
+ const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours');
+
+ nonDefaultWindows.forEach(window => {
+ const params = getTimeDiff(timeWindows[window]);
+ const diff = params.end - params.start;
+
+ // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs)
+ expect(diff).not.toEqual(28800);
+ expect(typeof diff).toEqual('number');
+ });
+ });
+});
diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
index f8107dd40b9..4db829b1e7b 100644
--- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
@@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201
described_class.new.perform(100, 101)
- expect(stages.all.pluck(:position)).to eq [2, 3]
+ expect(stages.all.order(:id).pluck(:position)).to eq [2, 3]
end
end
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 7d3d8a949ef..1d0ffb5e9df 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do
end
end
end
+
+ describe '.dead_jobs?' do
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(described_class.dead_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(described_class.dead_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
+
+ describe '.retrying_jobs?' do
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::RetrySet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(described_class.retrying_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(described_class.retrying_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 3c0007f4d57..0bc9e8bd3cd 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong action type is used' do
+ let(:config) do
+ { name: 'production',
+ action: ['stop'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong action type' do
+ expect(entry.errors)
+ .to include 'environment action should be a string'
+ end
+ end
+ end
+
context 'when invalid action is used' do
let(:config) do
{ name: 'production',
@@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong url type is used' do
+ let(:config) do
+ { name: 'production',
+ url: ['https://meow.meow'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong url type' do
+ expect(entry.errors)
+ .to include 'environment url should be a string'
+ end
+ end
+ end
+
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_COMMIT_REF_NAME',
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 8b39c4e4dd0..b7b30e60d44 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -615,7 +615,7 @@ module Gitlab
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) }
context "when validating a ci config file with no project context" do
- context "when a single string is provided" do
+ context "when a single string is provided", :quarantine do
let(:include_content) { "/local.gitlab-ci.yml" }
it "does not return any error" do
diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
new file mode 100644
index 00000000000..cc0f2baf905
--- /dev/null
+++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Import::MergeRequestHelpers, type: :helper do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ describe '.create_merge_request_without_hooks' do
+ let(:iid) { 42 }
+
+ let(:attributes) do
+ {
+ iid: iid,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'master-42',
+ target_branch: 'master',
+ state: :merged,
+ author_id: user.id,
+ assignee_id: user.id
+ }
+ end
+
+ subject { helper.create_merge_request_without_hooks(project, attributes, iid) }
+
+ context 'when merge request does not exist' do
+ it 'returns a new object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+
+ it 'does load all existing objects' do
+ 5.times do |iid|
+ MergeRequest.create!(
+ attributes.merge(iid: iid, source_branch: iid.to_s))
+ end
+
+ # does ensure that we only load object twice
+ # 1. by #insert_and_return_id
+ # 2. by project.merge_requests.find
+ expect_any_instance_of(MergeRequest).to receive(:attributes)
+ .twice.times.and_call_original
+
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+ end
+
+ context 'when merge request does exist' do
+ before do
+ MergeRequest.create!(attributes)
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(true)
+ end
+ end
+
+ context 'when project is deleted' do
+ before do
+ project.delete
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 082e3b36dd0..c57b96fb00d 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
expected = {
project: project,
tag: 'v1.0.0',
+ name: 'First release',
description: 'Release v1.0.0',
created_at: created_at,
updated_at: created_at
diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb
new file mode 100644
index 00000000000..fc9e421bea6
--- /dev/null
+++ b/spec/lib/gitlab/push_options_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PushOptions do
+ describe 'namespace and key validation' do
+ it 'ignores unrecognised namespaces' do
+ options = described_class.new(['invalid.key=value'])
+
+ expect(options.get(:invalid)).to eq(nil)
+ end
+
+ it 'ignores unrecognised keys' do
+ options = described_class.new(['merge_request.key=value'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'ignores blank keys' do
+ options = described_class.new(['merge_request'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'parses recognised namespace and key pairs' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request)).to include({
+ target: 'value'
+ })
+ end
+ end
+
+ describe '#get' do
+ it 'can emulate Hash#dig' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+ end
+
+ describe '#as_json' do
+ it 'returns all options' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.as_json).to include(
+ merge_request: {
+ target: 'value'
+ }
+ )
+ end
+ end
+
+ it 'can parse multiple push options' do
+ options = described_class.new([
+ 'merge_request.create',
+ 'merge_request.target=value'
+ ])
+
+ expect(options.get(:merge_request)).to include({
+ create: true,
+ target: 'value'
+ })
+ expect(options.get(:merge_request, :create)).to eq(true)
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'stores options internally as a HashWithIndifferentAccess' do
+ options = described_class.new([
+ 'merge_request.create'
+ ])
+
+ expect(options.get('merge_request', 'create')).to eq(true)
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'selects the last option when options contain duplicate namespace and key pairs' do
+ options = described_class.new([
+ 'merge_request.target=value1',
+ 'merge_request.target=value2'
+ ])
+
+ expect(options.get(:merge_request, :target)).to eq('value2')
+ end
+
+ it 'defaults values to true' do
+ options = described_class.new(['merge_request.create'])
+
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'expands aliases' do
+ options = described_class.new(['mr.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'forgives broken push options' do
+ options = described_class.new(['merge_request . target = value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index e65f97df3c3..43954511858 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -82,6 +82,13 @@ describe InstanceConfiguration do
it 'returns the key artifacts_max_size' do
expect(gitlab_ci.keys).to include(:artifacts_max_size)
end
+
+ it 'returns the key artifacts_max_size with values' do
+ stub_application_setting(max_artifacts_size: 200)
+
+ expect(gitlab_ci[:artifacts_max_size][:default]).to eq(100.megabytes)
+ expect(gitlab_ci[:artifacts_max_size][:value]).to eq(200.megabytes)
+ end
end
end
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index b4b32c95dee..0b19a4f8efc 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -18,6 +18,22 @@ RSpec.describe Release do
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:name) }
+
+ context 'when a release exists in the database without a name' do
+ it 'does not require name' do
+ existing_release_without_name = build(:release, project: project, author: user, name: nil)
+ existing_release_without_name.save(validate: false)
+
+ existing_release_without_name.description = "change"
+ existing_release_without_name.save
+ existing_release_without_name.reload
+
+ expect(existing_release_without_name).to be_valid
+ expect(existing_release_without_name.description).to eq("change")
+ expect(existing_release_without_name.name).to be_nil
+ end
+ end
end
describe '#assets_count' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 493d3642255..8fc7fdc8632 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -32,6 +32,7 @@ describe API::Environments do
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
+ expect(json_response.first).not_to have_key("last_deployment")
end
end
@@ -188,4 +189,25 @@ describe API::Environments do
end
end
end
+
+ describe 'GET /projects/:id/environments/:environment_id' do
+ context 'as member of the project' do
+ it 'returns project environments' do
+ create(:deployment, :success, project: project, environment: environment)
+
+ get api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/environment')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index 708a000532b..f95f460fd14 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -3,14 +3,24 @@ require 'spec_helper'
describe 'GitlabSchema configurations' do
include GraphqlHelpers
- let(:project) { create(:project, :repository) }
- let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) }
+ it 'shows an error if complexity is too high' do
+ project = create(:project, :repository)
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, "id\nname\ndescription")
- it 'shows an error if complexity it too high' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
post_graphql(query, current_user: nil)
expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
end
+
+ context 'when IntrospectionQuery' do
+ it 'is not too complex' do
+ query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql'))
+
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 0919540e4ba..1ce8f520962 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -888,8 +888,10 @@ describe API::Internal do
}
end
+ let(:branch_name) { 'feature' }
+
let(:changes) do
- "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch"
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
let(:push_options) do
@@ -905,9 +907,9 @@ describe API::Internal do
it 'enqueues a PostReceive worker job' do
expect(PostReceive).to receive(:perform_async)
- .with(gl_repository, identifier, changes, push_options)
+ .with(gl_repository, identifier, changes, { ci: { skip: true } })
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
end
it 'decreases the reference counter and returns the result' do
@@ -915,17 +917,17 @@ describe API::Internal do
.and_return(reference_counter)
expect(reference_counter).to receive(:decrease).and_return(true)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['reference_counter_decreased']).to be(true)
end
it 'returns link to create new merge request' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to match [{
- "branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "branch_name" => branch_name,
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}",
"new_merge_request" => true
}]
end
@@ -933,16 +935,87 @@ describe API::Internal do
it 'returns empty array if printing_merge_request_link_enabled is false' do
project.update!(printing_merge_request_link_enabled: false)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to eq([])
end
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ context 'when there are merge_request push options' do
+ before do
+ valid_params[:push_options] = ['merge_request.create']
+ end
+
+ it 'invokes MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ it 'creates a new merge request' do
+ expect do
+ post api('/internal/post_receive'), params: valid_params
+ end.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'links to the newly created merge request' do
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['merge_request_urls']).to match [{
+ 'branch_name' => branch_name,
+ 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1",
+ 'new_merge_request' => false
+ }]
+ end
+
+ it 'adds errors on the service instance to warnings' do
+ expect_any_instance_of(
+ MergeRequests::PushOptionsHandlerService
+ ).to receive(:errors).at_least(:once).and_return(['my error'])
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ context 'when the feature is disabled' do
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ Feature.disable(:mr_push_options)
+
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ expect do
+ post api('/internal/post_receive'), params: valid_params
+ end.not_to change { MergeRequest.count }
+
+ Feature.enable(:mr_push_options)
+ end
+ end
+ end
+
context 'broadcast message exists' do
let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
it 'returns one broadcast message' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(broadcast_message.message)
@@ -951,7 +1024,7 @@ describe API::Internal do
context 'broadcast message does not exist' do
it 'returns empty string' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -962,7 +1035,7 @@ describe API::Internal do
it 'returns empty string' do
allow(BroadcastMessage).to receive(:current).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -974,7 +1047,7 @@ describe API::Internal do
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz')
project_moved.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["redirected_message"]).to be_present
@@ -987,7 +1060,7 @@ describe API::Internal do
project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http')
project_created.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["project_created_message"]).to be_present
@@ -999,7 +1072,7 @@ describe API::Internal do
it 'does not try to notify that project moved' do
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 5fdc7c64030..3585a827838 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1734,7 +1734,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
it 'download artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h).to include download_headers
end
end
diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
deleted file mode 100644
index bc9747d1413..00000000000
--- a/spec/services/after_branch_delete_service_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'spec_helper'
-
-describe AfterBranchDeleteService do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'stops environments attached to branch' do
- expect(service).to receive(:stop_environments)
-
- service.execute('feature')
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 866d709d446..101b91e9cd8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -418,8 +418,7 @@ describe Ci::CreatePipelineService do
context 'when push options contain ci.skip' do
let(:push_options) do
- ['ci.skip',
- 'another push option']
+ { 'ci' => { 'skip' => true } }
end
it 'creates a pipline in the skipped state' do
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
new file mode 100644
index 00000000000..bb87267db7d
--- /dev/null
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -0,0 +1,339 @@
+require 'spec_helper'
+
+describe Git::BranchHooksService do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ let(:branch) { project.default_branch }
+ let(:ref) { "refs/heads/#{branch}" }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:oldrev) { commit.parent_id }
+ let(:newrev) { commit.id }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ describe "Git Push Data" do
+ subject(:push_data) { service.execute }
+
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'push',
+ before: oldrev,
+ after: newrev,
+ ref: ref,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
+
+ it 'includes expected commit data' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'includes expected author data' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Push Event' do
+ let(:event) { Event.find_by_action(Event::PUSHED) }
+
+ before do
+ service.execute
+ end
+
+ context "with an existing branch" do
+ it 'generates a push event with one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(1)
+ end
+ end
+
+ context "with a new branch" do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with more than one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to be_nil
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to be > 1
+ end
+ end
+
+ context 'removing a branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with no commits' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to be_nil
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(0)
+ end
+ end
+ end
+
+ describe 'Invalidating project cache' do
+ let(:commit_id) do
+ project.repository.update_file(
+ user, 'README.md', '', message: 'Update', branch_name: branch
+ )
+ end
+
+ let(:commit) { project.repository.commit(commit_id) }
+ let(:blank_sha) { Gitlab::Git::BLANK_SHA }
+
+ def clears_cache(extended: [])
+ expect(ProjectCacheWorker)
+ .to receive(:perform_async)
+ .with(project.id, extended, %i[commit_count repository_size])
+
+ service.execute
+ end
+
+ def clears_extended_cache
+ clears_cache(extended: %i[readme])
+ end
+
+ context 'on default branch' do
+ context 'create' do
+ # FIXME: When creating the default branch,the cache worker runs twice
+ before do
+ allow(ProjectCacheWorker).to receive(:perform_async)
+ end
+
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_extended_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ # TODO: this case should pass, but we only take account of added files
+ it { clears_cache }
+ end
+ end
+
+ context 'on ordinary branch' do
+ let(:branch) { 'fix' }
+
+ context 'create' do
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ it { clears_cache }
+ end
+ end
+ end
+
+ describe 'GPG signatures' do
+ context 'when the commit has a signature' do
+ context 'when the signature is already cached' do
+ before do
+ create(:gpg_signature, commit_sha: commit.id)
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'when the signature is not yet cached' do
+ it 'queues a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id)
+
+ service.execute
+ end
+
+ it 'can queue several commits to create the gpg signature' do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .and_return([sample_commit.id, another_sample_commit.id])
+
+ expect(CreateGpgSignatureWorker)
+ .to receive(:perform_async)
+ .with([sample_commit.id, another_sample_commit.id], project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when the commit does not have a signature' do
+ before do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .with(project.repository, [sample_commit.id])
+ .and_return([])
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker)
+ .not_to receive(:perform_async)
+ .with(sample_commit.id, project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ describe 'Processing commit messages' do
+ # Create 4 commits, 2 of which have references. Limiting to 2 commits, we
+ # expect to see one commit message processor enqueued.
+ let(:commit_ids) do
+ Array.new(4) do |i|
+ message = "Issue #{'#' if i.even?}#{i}"
+ project.repository.update_file(
+ user, 'README.md', '', message: message, branch_name: branch
+ )
+ end
+ end
+
+ let(:oldrev) { commit_ids.first }
+ let(:newrev) { commit_ids.last }
+
+ before do
+ stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2)
+ end
+
+ context 'creating the default branch' do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'updating the default branch' do
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing the default branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'creating a normal branch' do
+ let(:branch) { 'fix' }
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'updating a normal branch' do
+ let(:branch) { 'fix' }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing a normal branch' do
+ let(:branch) { 'fix' }
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index d0e2169b4a6..322e40a8112 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -8,7 +8,8 @@ describe Git::BranchPushService, services: true do
let(:blankrev) { Gitlab::Git::BLANK_SHA }
let(:oldrev) { sample_commit.parent_id }
let(:newrev) { sample_commit.id }
- let(:ref) { 'refs/heads/master' }
+ let(:branch) { 'master' }
+ let(:ref) { "refs/heads/#{branch}" }
before do
project.add_maintainer(user)
@@ -132,64 +133,6 @@ describe Git::BranchPushService, services: true do
end
end
- describe "Git Push Data" do
- let(:commit) { project.commit(newrev) }
-
- subject { push_data_from_service(project, user, oldrev, newrev, ref) }
-
- it { is_expected.to include(object_kind: 'push') }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
-
describe "Pipelines" do
subject { execute_service(project, user, oldrev, newrev, ref) }
@@ -203,59 +146,13 @@ describe Git::BranchPushService, services: true do
end
end
- describe "Push Event" do
- context "with an existing branch" do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
-
- it 'generates a push event with one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to eq(oldrev)
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to eq(1)
- end
- end
-
- context "with a new branch" do
- let!(:new_branch_data) { push_data_from_service(project, user, Gitlab::Git::BLANK_SHA, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
-
- it 'generates a push event with more than one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to be_nil
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to be > 1
- end
- end
-
- context "Updates merge requests" do
- it "when pushing a new branch for the first time" do
- expect(UpdateMergeRequestsWorker).to receive(:perform_async)
- .with(project.id, user.id, blankrev, 'newrev', ref)
- execute_service(project, user, blankrev, 'newrev', ref )
- end
- end
-
- describe 'system hooks' do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let!(:system_hooks_service) { SystemHooksService.new }
+ describe "Updates merge requests" do
+ it "when pushing a new branch for the first time" do
+ expect(UpdateMergeRequestsWorker)
+ .to receive(:perform_async)
+ .with(project.id, user.id, blankrev, 'newrev', ref)
- it "sends a system hook after pushing a branch" do
- allow(SystemHooksService).to receive(:new).and_return(system_hooks_service)
- allow(system_hooks_service).to receive(:execute_hooks)
-
- execute_service(project, user, oldrev, newrev, ref)
-
- expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks)
- end
+ execute_service(project, user, blankrev, 'newrev', ref )
end
end
@@ -700,125 +597,64 @@ describe Git::BranchPushService, services: true do
end
end
- describe '#update_caches' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
-
- context 'on the default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(true)
- end
-
- it 'flushes the caches of any special files that have been changed' do
- commit = double(:commit)
- diff = double(:diff, new_path: 'README.md')
-
- expect(commit).to receive(:raw_deltas)
- .and_return([diff])
-
- service.push_commits = [commit]
+ describe "CI environments" do
+ context 'create branch' do
+ let(:oldrev) { blankrev }
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, %i(readme), %i(commit_count repository_size))
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'on a non-default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(false)
- end
-
- it 'does not flush any conditional caches' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], %i(commit_count repository_size))
- .and_call_original
+ context 'update branch' do
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- end
-
- describe '#process_commit_messages' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
- it 'only schedules a limited number of commits' do
- service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))
-
- expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
-
- service.process_commit_messages
- end
-
- it "skips commits which don't include cross-references" do
- service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]
-
- expect(ProcessCommitWorker).not_to receive(:perform_async)
-
- service.process_commit_messages
- end
- end
-
- describe '#update_signatures' do
- let(:service) do
- described_class.new(
- project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: 'refs/heads/master'
- )
- end
+ context 'delete branch' do
+ let(:newrev) { blankrev }
- context 'when the commit has a signature' do
- context 'when the signature is already cached' do
- before do
- create(:gpg_signature, commit_sha: sample_commit.id)
+ it 'stops environments' do
+ expect_next_instance_of(::Ci::StopEnvironmentsService) do |stop_service|
+ expect(stop_service.project).to eq(project)
+ expect(stop_service.current_user).to eq(user)
+ expect(stop_service).to receive(:execute).with(branch)
end
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
+ end
+ end
- context 'when the signature is not yet cached' do
- it 'queues a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id], project.id)
+ describe 'Hooks' do
+ context 'run on a branch' do
+ it 'delegates to Git::BranchHooksService' do
+ expect_next_instance_of(::Git::BranchHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(project)
+ expect(hooks_service.current_user).to eq(user)
+ expect(hooks_service.params).to include(
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ )
- execute_service(project, user, oldrev, newrev, ref)
+ expect(hooks_service).to receive(:execute)
end
- it 'can queue several commits to create the gpg signature' do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).and_return([sample_commit.id, another_sample_commit.id])
-
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id, another_sample_commit.id], project.id)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'when the commit does not have a signature' do
- before do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([])
- end
+ context 'run on a tag' do
+ let(:ref) { 'refs/tags/v1.1.0' }
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id)
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
execute_service(project, user, oldrev, newrev, ref)
end
@@ -830,8 +666,4 @@ describe Git::BranchPushService, services: true do
service.execute
service
end
-
- def push_data_from_service(project, user, oldrev, newrev, ref)
- execute_service(project, user, oldrev, newrev, ref).push_data
- end
end
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
new file mode 100644
index 00000000000..8f91ce3b4c5
--- /dev/null
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Git::TagHooksService, :service do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { "refs/tags/#{tag_name}" }
+ let(:tag_name) { 'v1.1.0' }
+
+ let(:tag) { project.repository.find_tag(tag_name) }
+ let(:commit) { tag.dereferenced_target }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ describe 'System hooks' do
+ it 'Executes system hooks' do
+ push_data = service.execute
+
+ expect_next_instance_of(SystemHooksService) do |system_hooks_service|
+ expect(system_hooks_service)
+ .to receive(:execute_hooks)
+ .with(push_data, :tag_push_hooks)
+ end
+
+ service.execute
+ end
+ end
+
+ describe "Webhooks" do
+ it "executes hooks on the project" do
+ expect(project).to receive(:execute_hooks)
+
+ service.execute
+ end
+ end
+
+ describe "Pipelines" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ project.add_developer(user)
+ end
+
+ it "creates a new pipeline" do
+ expect { service.execute }.to change { Ci::Pipeline.count }
+
+ expect(Ci::Pipeline.last).to be_push
+ end
+ end
+
+ describe 'Push data' do
+ shared_examples_for 'tag push data expectations' do
+ subject(:push_data) { service.execute }
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'tag_push',
+ ref: ref,
+ before: oldrev,
+ after: newrev,
+ message: tag.message,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected repository attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { is_expected.to include(timestamp: commit.date.xmlschema) }
+
+ it 'has expected commit attributes' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with an author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'has expected author attributes' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'annotated tag' do
+ include_examples 'tag push data expectations'
+ end
+
+ context 'lightweight tag' do
+ let(:tag_name) { 'light-tag' }
+ let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+
+ before do
+ # Create the lightweight tag
+ rugged_repo(project.repository).tags.create(tag_name, newrev)
+
+ # Clear tag list cache
+ project.repository.expire_tags_cache
+ end
+
+ include_examples 'tag push data expectations'
+ end
+ end
+end
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 2d960fc9f08..5e89a912060 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -31,178 +31,27 @@ describe Git::TagPushService do
end
end
- describe 'System Hooks' do
- let!(:push_data) { service.tap(&:execute).push_data }
-
- it "executes system hooks after pushing a tag" do
- expect_next_instance_of(SystemHooksService) do |system_hooks_service|
- expect(system_hooks_service)
- .to receive(:execute_hooks)
- .with(push_data, :tag_push_hooks)
- end
-
- service.execute
- end
- end
-
- describe "Pipelines" do
- subject { service.execute }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- project.add_developer(user)
- end
-
- it "creates a new pipeline" do
- expect { subject }.to change { Ci::Pipeline.count }
- expect(Ci::Pipeline.last).to be_push
- end
- end
-
- describe "Git Tag Push Data" do
- subject { @push_data }
- let(:tag) { project.repository.find_tag(tag_name) }
- let(:commit) { tag.dereferenced_target }
-
- context 'annotated tag' do
- let(:tag_name) { Gitlab::Git.ref_name(ref) }
-
- before do
- service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
+ describe 'Hooks' do
+ context 'run on a tag' do
+ it 'delegates to Git::TagHooksService' do
+ expect_next_instance_of(::Git::TagHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(service.project)
+ expect(hooks_service.current_user).to eq(service.current_user)
+ expect(hooks_service.params).to eq(service.params)
+
+ expect(hooks_service).to receive(:execute)
end
- end
- end
-
- context 'lightweight tag' do
- let(:tag_name) { 'light-tag' }
- let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
- let(:ref) { "refs/tags/light-tag" }
-
- before do
- # Create the lightweight tag
- rugged_repo(project.repository).tags.create(tag_name, newrev)
-
- # Clear tag list cache
- project.repository.expire_tags_cache
service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
end
end
- end
- describe "Webhooks" do
- context "execute webhooks" do
- let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') }
+ context 'run on a branch' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
- it "when pushing tags" do
- expect(project).to receive(:execute_hooks)
service.execute
end
end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
new file mode 100644
index 00000000000..686b4b49f24
--- /dev/null
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -0,0 +1,404 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::PushOptionsHandlerService do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(project, user, changes, push_options) }
+ let(:source_branch) { 'fix' }
+ let(:target_branch) { 'feature' }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples_for 'a service that can create a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'sets the correct target branch' do
+ branch = push_options[:target] || project.default_branch
+
+ service.execute
+
+ expect(last_mr.target_branch).to eq(branch)
+ end
+
+ it 'assigns the MR to the user' do
+ service.execute
+
+ expect(last_mr.assignee).to eq(user)
+ end
+
+ context 'when project has been forked' do
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(forked_project, user, changes, push_options) }
+
+ before do
+ allow(forked_project).to receive(:empty_repo?).and_return(false)
+ end
+
+ it 'sets the correct source project' do
+ service.execute
+
+ expect(last_mr.source_project).to eq(forked_project)
+ end
+
+ it 'sets the correct target project' do
+ service.execute
+
+ expect(last_mr.target_project).to eq(project)
+ end
+ end
+ end
+
+ shared_examples_for 'a service that can set the target of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets the target_branch' do
+ service.execute
+
+ expect(last_mr.target_branch).to eq(target_branch)
+ end
+ end
+
+ shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets merge_when_pipeline_succeeds' do
+ service.execute
+
+ expect(last_mr.merge_when_pipeline_succeeds).to eq(true)
+ end
+
+ it 'sets merge_user to the user' do
+ service.execute
+
+ expect(last_mr.merge_user).to eq(user)
+ end
+ end
+
+ shared_examples_for 'a service that does not create a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.count }
+ end
+ end
+
+ shared_examples_for 'a service that does not update a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) }
+ end
+ end
+
+ shared_examples_for 'a service that does nothing' do
+ include_examples 'a service that does not create a merge request'
+ include_examples 'a service that does not update a merge request'
+ end
+
+ describe '`create` push option' do
+ let(:push_options) { { create: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`merge_when_pipeline_succeeds` push option' do
+ let(:push_options) { { merge_when_pipeline_succeeds: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`target` push option' do
+ let(:push_options) { { target: target_branch } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe 'multiple pushed branches' do
+ let(:push_options) { { create: true } }
+ let(:changes) do
+ [
+ new_branch_changes,
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/feature_conflict"
+ ]
+ end
+
+ it 'creates a merge request per branch' do
+ expect { service.execute }.to change { MergeRequest.count }.by(2)
+ end
+
+ context 'when there are too many pushed branches' do
+ let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT }
+ let(:changes) do
+ TestEnv::BRANCH_SHA.to_a[0..limit].map do |x|
+ "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}"
+ end
+ end
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"])
+ end
+ end
+ end
+
+ describe 'no push options' do
+ let(:push_options) { {} }
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ describe 'no user' do
+ let(:user) { nil }
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['User is required'])
+ end
+ end
+
+ describe 'unauthorized user' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id))
+
+ service.execute
+
+ expect(service.errors).to eq(['User access was denied'])
+ end
+ end
+
+ describe 'handling unexpected exceptions' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+ let(:exception) { StandardError.new('My standard error') }
+
+ def run_service_with_exception
+ allow_any_instance_of(
+ MergeRequests::BuildService
+ ).to receive(:execute).and_raise(exception)
+
+ service.execute
+ end
+
+ it 'records an error' do
+ run_service_with_exception
+
+ expect(service.errors).to eq(['An unknown error occurred'])
+ end
+
+ it 'writes to Gitlab::AppLogger' do
+ expect(Gitlab::AppLogger).to receive(:error).with(exception)
+
+ run_service_with_exception
+ end
+ end
+
+ describe 'when target is not a valid branch name' do
+ let(:push_options) { { create: true, target: 'my-branch' } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['Branch my-branch does not exist'])
+ end
+ end
+
+ describe 'when MRs are not enabled' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ expect(project).to receive(:merge_requests_enabled?).and_return(false)
+
+ service.execute
+
+ expect(service.errors).to eq(["Merge requests are not enabled for project #{project.full_path}"])
+ end
+ end
+
+ describe 'when MR has ActiveRecord errors' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'adds the error to its errors property' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ service.execute
+
+ expect(service.errors).to eq(['my error'])
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index bd10523bc94..5ed06df7072 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -146,7 +146,10 @@ describe MergeRequests::RefreshService do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
- subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+ subject { service.new(project, @user).execute(@oldrev, @newrev, ref) }
+
+ let(:ref) { 'refs/heads/master' }
+ let(:project) { @project }
context "when .gitlab-ci.yml has merge_requests keywords" do
let(:config) do
@@ -162,14 +165,17 @@ describe MergeRequests::RefreshService do
it 'create detached merge request pipeline with commits' do
expect { subject }
.to change { @merge_request.merge_request_pipelines.count }.by(1)
- .and change { @fork_merge_request.merge_request_pipelines.count }.by(1)
.and change { @another_merge_request.merge_request_pipelines.count }.by(0)
expect(@merge_request.has_commits?).to be_truthy
- expect(@fork_merge_request.has_commits?).to be_truthy
expect(@another_merge_request.has_commits?).to be_falsy
end
+ it 'does not create detached merge request pipeline for forked project' do
+ expect { subject }
+ .not_to change { @fork_merge_request.merge_request_pipelines.count }
+ end
+
it 'create detached merge request pipeline for non-fork merge request' do
subject
@@ -177,11 +183,25 @@ describe MergeRequests::RefreshService do
.to be_detached_merge_request_pipeline
end
- it 'create legacy detached merge request pipeline for fork merge request' do
- subject
+ context 'when service is hooked by target branch' do
+ let(:ref) { 'refs/heads/feature' }
- expect(@fork_merge_request.merge_request_pipelines.first)
- .to be_legacy_detached_merge_request_pipeline
+ it 'does not create detached merge request pipeline' do
+ expect { subject }
+ .not_to change { @merge_request.merge_request_pipelines.count }
+ end
+ end
+
+ context 'when service runs on forked project' do
+ let(:project) { @fork_project }
+
+ it 'creates legacy detached merge request pipeline for fork merge request' do
+ expect { subject }
+ .to change { @fork_merge_request.merge_request_pipelines.count }.by(1)
+
+ expect(@fork_merge_request.merge_request_pipelines.first)
+ .to be_legacy_detached_merge_request_pipeline
+ end
end
context 'when ci_use_merge_request_ref feature flag is false' do
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index a6cc2251e48..f6ee15f750c 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -21,16 +21,20 @@ describe NoteSummary do
describe '#note' do
it 'returns note hash' do
- expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note')
+ Timecop.freeze do
+ expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note',
+ created_at: Time.now)
+ end
end
context 'when noteable is a commit' do
- let(:noteable) { build(:commit) }
+ let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) }
it 'returns note hash specific to commit' do
expect(create_note_summary.note).to eq(
noteable: nil, project: project, author: user, note: 'note',
- noteable_type: 'Commit', commit_id: noteable.id
+ noteable_type: 'Commit', commit_id: noteable.id,
+ created_at: Time.at(43)
)
end
end
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
new file mode 100644
index 00000000000..7e351c9ce54
--- /dev/null
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Projects::UpdateStatisticsService do
+ let(:service) { described_class.new(project, nil, statistics: statistics)}
+ let(:statistics) { %w(repository_size) }
+
+ describe '#execute' do
+ context 'with a non-existing project' do
+ let(:project) { nil }
+
+ it 'does nothing' do
+ expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
+
+ service.execute
+ end
+ end
+
+ context 'with an existing project without a repository' do
+ let(:project) { create(:project) }
+
+ it 'does nothing' do
+ expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
+
+ service.execute
+ end
+ end
+
+ context 'with an existing project with a repository' do
+ let(:project) { create(:project, :repository) }
+
+ it 'refreshes the project statistics' do
+ expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
+ .with(only: statistics.map(&:to_sym))
+ .and_call_original
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 612e9f152e7..0efe37f1167 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -19,6 +19,8 @@ describe Releases::CreateService do
shared_examples 'a successful release creation' do
it 'creates a new release' do
result = service.execute
+
+ expect(project.releases.count).to eq(1)
expect(result[:status]).to eq(:success)
expect(result[:tag]).not_to be_nil
expect(result[:release]).not_to be_nil
@@ -69,4 +71,12 @@ describe Releases::CreateService do
end
end
end
+
+ describe '#find_or_build_release' do
+ it 'does not save the built release' do
+ service.find_or_build_release
+
+ expect(project.releases.count).to eq(0)
+ end
+ end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b917de14b2e..13d7d795703 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemNoteService do
+ include ProjectForksHelper
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
@@ -11,6 +14,14 @@ describe SystemNoteService do
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
+ shared_examples_for 'a note with overridable created_at' do
+ let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) }
+
+ it 'the note has the correct time' do
+ expect(subject.created_at).to eq Time.at(42)
+ end
+ end
+
shared_examples_for 'a system note' do
let(:expected_noteable) { noteable }
let(:commit_count) { nil }
@@ -137,6 +148,8 @@ describe SystemNoteService do
end
context 'when assignee added' do
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "assigned to @#{assignee.username}"
end
@@ -145,6 +158,8 @@ describe SystemNoteService do
context 'when assignee removed' do
let(:assignee) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq 'removed assignee'
end
@@ -168,6 +183,8 @@ describe SystemNoteService do
described_class.change_issue_assignees(issue, project, author, old_assignees).note
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
end
@@ -213,6 +230,8 @@ describe SystemNoteService do
expect(subject.note).to eq "changed milestone to #{reference}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -221,6 +240,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
@@ -237,6 +258,8 @@ describe SystemNoteService do
it 'sets the note text to use the milestone name' do
expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -245,6 +268,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
end
@@ -254,6 +279,8 @@ describe SystemNoteService do
let(:due_date) { Date.today }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'due_date' }
end
@@ -280,6 +307,8 @@ describe SystemNoteService do
let(:status) { 'reopened' }
let(:source) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'opened' }
end
@@ -289,6 +318,8 @@ describe SystemNoteService do
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456"
end
@@ -338,6 +369,8 @@ describe SystemNoteService do
let(:action) { 'title' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note)
.to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
@@ -353,6 +386,8 @@ describe SystemNoteService do
let(:action) { 'description' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq('changed the description')
end
@@ -478,6 +513,8 @@ describe SystemNoteService do
let(:action) { 'cross_reference' }
end
+ it_behaves_like 'a note with overridable created_at'
+
describe 'note_body' do
context 'cross-project' do
let(:project2) { create(:project, :repository) }
@@ -619,7 +656,7 @@ describe SystemNoteService do
context 'commit with cross-reference from fork' do
let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
- let(:forked_project) { Projects::ForkService.new(project, author2).execute }
+ let(:forked_project) { fork_project(project, author2, repository: true) }
let(:commit2) { forked_project.commit }
before do
@@ -896,6 +933,28 @@ describe SystemNoteService do
end
end
+ describe '.change_time_estimate' do
+ subject { described_class.change_time_estimate(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'time_tracking' }
+ end
+
+ context 'with a time estimate' do
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ end
+ end
+
+ context 'without a time estimate' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed time estimate"
+ end
+ end
+ end
+
describe '.discussion_continued_in_issue' do
let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
let(:merge_request) { discussion.noteable }
@@ -922,28 +981,6 @@ describe SystemNoteService do
end
end
- describe '.change_time_estimate' do
- subject { described_class.change_time_estimate(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'time_tracking' }
- end
-
- context 'with a time estimate' do
- it 'sets the note text' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
- end
- end
-
- context 'without a time estimate' do
- it 'sets the note text' do
- expect(subject.note).to eq "removed time estimate"
- end
- end
- end
-
describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 60db3e1bc46..953cf1519bb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -100,8 +100,8 @@ RSpec.configure do |config|
config.include PolicyHelpers, type: :policy
if ENV['CI']
- # This includes the first try, i.e. tests will be run 4 times before failing.
- config.default_retry_count = 4
+ # This includes the first try, i.e. tests will be run 2 times before failing.
+ config.default_retry_count = 2
config.reporter.register_listener(
RspecFlaky::Listener.new,
:example_passed,
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 6569feec39b..03057a102c5 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -149,4 +149,10 @@ module FilteredSearchHelpers
loop until find('.filtered-search').value.strip == text
end
end
+
+ def close_dropdown_menu_if_visible
+ find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
+ toggle.click if toggle.visible?
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
index db3ecccc339..ae78cd86cd5 100644
--- a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
@@ -1,25 +1,35 @@
# frozen_string_literal: true
-shared_examples 'due quick action not available' do
- it 'does not set the due date' do
- add_note('/due 2016-08-28')
+shared_examples 'due quick action' do
+ context 'due quick action available and date can be added' do
+ it 'sets the due date accordingly' do
+ add_note('/due 2016-08-28')
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content '/due 2016-08-28'
- end
-end
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Commands applied'
+
+ visit project_issue_path(project, issue)
-shared_examples 'due quick action available and date can be added' do
- it 'sets the due date accordingly' do
- add_note('/due 2016-08-28')
+ page.within '.due_date' do
+ expect(page).to have_content 'Aug 28, 2016'
+ end
+ end
+ end
- expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Commands applied'
+ context 'due quick action not available' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
- visit project_issue_path(project, issue)
+ it 'does not set the due date' do
+ add_note('/due 2016-08-28')
- page.within '.due_date' do
- expect(page).to have_content 'Aug 28, 2016'
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content '/due 2016-08-28'
end
end
end
diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb
index b6fb2a66b0e..22fbfb48928 100644
--- a/spec/support/shared_examples/wiki_file_attachments_examples.rb
+++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb
@@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do
end
end
- context 'uploading is complete' do
+ context 'uploading is complete', :quarantine do
it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone
wait_for_requests
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 66958a4c116..a3fe8fa4501 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -63,8 +63,12 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést" }
it "calls Git::BranchPushService" do
- expect_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(Git::TagPushService).not_to receive(:execute)
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -73,8 +77,12 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/tags/tag" }
it "calls Git::TagPushService" do
- expect_any_instance_of(Git::BranchPushService).not_to receive(:execute)
- expect_any_instance_of(Git::TagPushService).to receive(:execute).and_return(true)
+ expect(Git::BranchPushService).not_to receive(:execute)
+
+ expect_next_instance_of(Git::TagPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -83,8 +91,9 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/merge-requests/123" }
it "does not call any of the services" do
- expect_any_instance_of(Git::BranchPushService).not_to receive(:execute)
- expect_any_instance_of(Git::TagPushService).not_to receive(:execute)
+ expect(Git::BranchPushService).not_to receive(:new)
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -127,7 +136,9 @@ describe PostReceive do
allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
# silence hooks so we can isolate
allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true)
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
end
it 'calls SystemHooksService' do
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index a7353227043..3c40269adc7 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -7,9 +7,9 @@ describe ProjectCacheWorker do
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
- let(:statistics) { project.statistics }
- let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_key) { ["project_cache_worker", project.id, *statistics.sort].join(":") }
let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
+ let(:statistics) { [] }
describe '#perform' do
before do
@@ -35,14 +35,6 @@ describe ProjectCacheWorker do
end
context 'with an existing project' do
- it 'updates the project statistics' do
- expect(worker).to receive(:update_statistics)
- .with(kind_of(Project), %i(repository_size))
- .and_call_original
-
- worker.perform(project.id, [], %w(repository_size))
- end
-
it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
.with(%i(readme))
@@ -51,6 +43,18 @@ describe ProjectCacheWorker do
worker.perform(project.id, %w(readme))
end
+ context 'with statistics' do
+ let(:statistics) { %w(repository_size) }
+
+ it 'updates the project statistics' do
+ expect(worker).to receive(:update_statistics)
+ .with(kind_of(Project), statistics)
+ .and_call_original
+
+ worker.perform(project.id, [], statistics)
+ end
+ end
+
context 'with plain readme' do
it 'refreshes the method caches' do
allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
@@ -66,25 +70,34 @@ describe ProjectCacheWorker do
end
describe '#update_statistics' do
+ let(:statistics) { %w(repository_size) }
+
context 'when a lease could not be obtained' do
- it 'does not update the repository size' do
+ it 'does not update the project statistics' do
stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(statistics).not_to receive(:refresh!)
+ expect(Projects::UpdateStatisticsService).not_to receive(:new)
- worker.update_statistics(project)
+ expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in)
+
+ worker.update_statistics(project, statistics)
end
end
context 'when a lease could be obtained' do
- it 'updates the project statistics' do
+ it 'updates the project statistics twice' do
stub_exclusive_lease(lease_key, timeout: lease_timeout)
- expect(statistics).to receive(:refresh!)
- .with(only: %i(repository_size))
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+ .twice
+
+ expect(UpdateProjectStatisticsWorker).to receive(:perform_in)
+ .with(lease_timeout, project.id, statistics)
.and_call_original
- worker.update_statistics(project, %i(repository_size))
+ worker.update_statistics(project, statistics)
end
end
end
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
new file mode 100644
index 00000000000..a268fd2e4ba
--- /dev/null
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe UpdateProjectStatisticsWorker do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project, :repository) }
+ let(:statistics) { %w(repository_size) }
+
+ describe '#perform' do
+ it 'updates the project statistics' do
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+
+ worker.perform(project.id, statistics)
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 571ac952e90..41758181610 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -658,10 +658,10 @@
eslint-plugin-promise "^4.1.1"
eslint-plugin-vue "^5.0.0"
-"@gitlab/svgs@^1.58.0":
- version "1.58.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f"
- integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg==
+"@gitlab/svgs@^1.59.0":
+ version "1.59.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087"
+ integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g==
"@gitlab/ui@^3.2.0":
version "3.2.0"