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:
authorAlex Hanselka <alex@gitlab.com>2018-12-11 00:42:50 +0300
committerAlex Hanselka <alex@gitlab.com>2018-12-11 00:42:50 +0300
commitf77888fe0857c9f4a47133f434c741a510946767 (patch)
treeb227b60894474ba2c9a8864b66e4df65763b35b8
parentd2120ff1e705799752e7d9704cae3f1896d8e186 (diff)
parent2d7dc668506a0576e231fbe290c89e47cf088300 (diff)
Merge branch '11-6-stable-prepare-rc5' into '11-6-stable'
Prepare 11.6 RC5 release See merge request gitlab-org/gitlab-ce!23704
-rw-r--r--.gitlab/CODEOWNERS (renamed from .gitlab/CODEOWNERS.disabled)0
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue82
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js6
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/diffs/store/utils.js3
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue62
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js5
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue16
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/projects/project.js9
-rw-r--r--app/assets/javascripts/terminal/index.js2
-rw-r--r--app/assets/javascripts/terminal/terminal.js57
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss10
-rw-r--r--app/assets/stylesheets/framework/mobile.scss9
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_mixins.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss17
-rw-r--r--app/assets/stylesheets/pages/builds.scss17
-rw-r--r--app/assets/stylesheets/pages/projects.scss187
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/clusters/applications_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/finders/issuable_finder.rb42
-rw-r--r--app/helpers/button_helper.rb5
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/sentry_helper.rb11
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/clusters/applications/cert_manager.rb4
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/concerns/discussion_on_diff.rb5
-rw-r--r--app/models/concerns/fast_destroy_all.rb5
-rw-r--r--app/models/concerns/with_uploads.rb31
-rw-r--r--app/models/merge_request.rb23
-rw-r--r--app/models/pool_repository.rb77
-rw-r--r--app/models/project.rb41
-rw-r--r--app/models/shard.rb4
-rw-r--r--app/models/upload.rb19
-rw-r--r--app/models/uploads/base.rb19
-rw-r--r--app/models/uploads/fog.rb43
-rw-r--r--app/models/uploads/local.rb56
-rw-r--r--app/presenters/project_presenter.rb140
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/diff_file_base_entity.rb101
-rw-r--r--app/serializers/diff_file_entity.rb98
-rw-r--r--app/serializers/discussion_diff_file_entity.rb4
-rw-r--r--app/serializers/discussion_entity.rb15
-rw-r--r--app/serializers/trigger_variable_entity.rb3
-rw-r--r--app/services/clusters/applications/create_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb22
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/notify/_note_email.html.haml23
-rw-r--r--app/views/notify/_note_email.text.erb13
-rw-r--r--app/views/projects/_files.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml130
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml4
-rw-r--r--app/views/projects/buttons/_clone.html.haml31
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml27
-rw-r--r--app/views/projects/buttons/_star.html.haml12
-rw-r--r--app/views/projects/empty.html.haml122
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/show.html.haml16
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml12
-rw-r--r--app/views/shared/members/_access_request_links.html.haml17
-rw-r--r--app/views/shared/notifications/_button.html.haml6
-rw-r--r--app/workers/all_queues.yml6
-rw-r--r--app/workers/concerns/object_pool_queue.rb12
-rw-r--r--app/workers/delete_stored_files_worker.rb22
-rw-r--r--app/workers/git_garbage_collect_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb9
-rw-r--r--app/workers/object_pool/create_worker.rb44
-rw-r--r--app/workers/object_pool/join_worker.rb20
-rw-r--r--app/workers/object_pool/schedule_join_worker.rb19
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb14
-rw-r--r--changelogs/unreleased/20422-hide-ui-variables-by-default.yml6
-rw-r--r--changelogs/unreleased/22548-reopen-error-message.yml6
-rw-r--r--changelogs/unreleased/48889-populate-merge_commit_sha.yml6
-rw-r--r--changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml5
-rw-r--r--changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml5
-rw-r--r--changelogs/unreleased/52007-frontmatter-toml-json.yml5
-rw-r--r--changelogs/unreleased/cert-manager-email.yml5
-rw-r--r--changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml5
-rw-r--r--changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml5
-rw-r--r--changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml5
-rw-r--r--changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml5
-rw-r--r--changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml5
-rw-r--r--changelogs/unreleased/sh-truncate-with-periods.yml5
-rw-r--r--changelogs/unreleased/store-correlation-logs.yml5
-rw-r--r--changelogs/unreleased/tc-backfill-hashed-project_repositories.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml5
-rw-r--r--changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml5
-rw-r--r--changelogs/unreleased/winh-milestone-select.yml5
-rw-r--r--changelogs/unreleased/zj-pool-repository-creation.yml5
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/correlation_id.rb3
-rw-r--r--config/initializers/lograge.rb1
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/initializers/sidekiq.rb3
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--danger/documentation/Dangerfile19
-rw-r--r--db/fixtures/development/04_project.rb14
-rw-r--r--db/fixtures/development/10_merge_requests.rb12
-rw-r--r--db/migrate/20181128123704_add_state_to_pool_repository.rb19
-rw-r--r--db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb11
-rw-r--r--db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb17
-rw-r--r--db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb26
-rw-r--r--db/schema.rb6
-rw-r--r--doc/administration/repository_storage_types.md17
-rw-r--r--doc/api/merge_requests.md5
-rw-r--r--doc/api/search.md28
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request.pngbin0 -> 57512 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/pipeline_detail.pngbin0 -> 42583 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md84
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin3637 -> 30193 bytes
-rw-r--r--doc/ci/variables/README.md145
-rw-r--r--doc/ci/yaml/README.md37
-rw-r--r--doc/development/documentation/index.md11
-rw-r--r--doc/development/feature_flags.md10
-rw-r--r--doc/topics/autodevops/index.md9
-rw-r--r--doc/user/group/clusters/index.md126
-rw-r--r--doc/user/group/index.md3
-rw-r--r--doc/user/project/clusters/index.md35
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/namespaces.rb17
-rw-r--r--lib/banzai/filter/front_matter_filter.rb34
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb66
-rw-r--r--lib/banzai/filter/yaml_front_matter_filter.rb27
-rw-r--r--lib/banzai/pipeline/pre_process_pipeline.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_hashed_project_repositories.rb134
-rw-r--r--lib/gitlab/branch_push_merge_commit_analyzer.rb132
-rw-r--r--lib/gitlab/correlation_id.rb40
-rw-r--r--lib/gitlab/database/migration_helpers.rb3
-rw-r--r--lib/gitlab/git/object_pool.rb62
-rw-r--r--lib/gitlab/git/repository.rb7
-rw-r--r--lib/gitlab/gitaly_client.rb1
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb45
-rw-r--r--lib/gitlab/grape_logging/loggers/correlation_id_logger.rb14
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/json_logger.rb1
-rw-r--r--lib/gitlab/middleware/correlation_id.rb35
-rw-r--r--lib/gitlab/sentry.rb13
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_injector.rb14
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_logger.rb15
-rw-r--r--locale/gitlab.pot118
-rw-r--r--spec/controllers/application_controller_spec.rb8
-rw-r--r--spec/controllers/groups_controller_spec.rb3
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb56
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb14
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb10
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/factories/pool_repositories.rb23
-rw-r--r--spec/features/projects/clusters/applications_spec.rb38
-rw-r--r--spec/features/projects/jobs_spec.rb87
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb15
-rw-r--r--spec/features/projects/show/developer_views_empty_project_instructions_spec.rb49
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb15
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb23
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb4
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb90
-rw-r--r--spec/features/tags/master_views_tags_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb127
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/job/trigger.json3
-rw-r--r--spec/initializers/lograge_spec.rb38
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js48
-rw-r--r--spec/javascripts/clusters/services/mock_data.js2
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js1
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js2
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js32
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js4
-rw-r--r--spec/javascripts/jobs/components/trigger_block_spec.js28
-rw-r--r--spec/javascripts/lib/utils/dom_utils_spec.js54
-rw-r--r--spec/lib/banzai/filter/front_matter_filter_spec.rb140
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb47
-rw-r--r--spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb53
-rw-r--r--spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb90
-rw-r--r--spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb62
-rw-r--r--spec/lib/gitlab/correlation_id_spec.rb77
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb89
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb46
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb6
-rw-r--r--spec/lib/gitlab/sentry_spec.rb22
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb47
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb35
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb28
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb86
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/pool_repository_spec.rb6
-rw-r--r--spec/models/project_spec.rb40
-rw-r--r--spec/models/uploads/fog_spec.rb69
-rw-r--r--spec/models/uploads/local_spec.rb45
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/presenters/project_presenter_spec.rb96
-rw-r--r--spec/requests/api/helpers_spec.rb30
-rw-r--r--spec/serializers/diff_file_entity_spec.rb33
-rw-r--r--spec/serializers/discussion_diff_file_entity_spec.rb39
-rw-r--r--spec/serializers/discussion_entity_spec.rb8
-rw-r--r--spec/serializers/trigger_variable_entity_spec.rb49
-rw-r--r--spec/services/ci/retry_build_service_spec.rb6
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb25
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb73
-rw-r--r--spec/services/projects/fork_service_spec.rb30
-rw-r--r--spec/support/helpers/test_env.rb3
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb60
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_examples.rb46
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/workers/object_pool/create_worker_spec.rb59
-rw-r--r--spec/workers/object_pool/join_worker_spec.rb35
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb16
-rw-r--r--spec/workers/remove_old_web_hook_logs_worker_spec.rb18
234 files changed, 4720 insertions, 1190 deletions
diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS
index a4b773b15a9..a4b773b15a9 100644
--- a/.gitlab/CODEOWNERS.disabled
+++ b/.gitlab/CODEOWNERS
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 665a9c77822..489615f1f78 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -84,6 +84,9 @@ export default {
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
+ certManagerInstalled() {
+ return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
+ },
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(
@@ -130,9 +133,9 @@ export default {
return sprintf(
_.escape(
s__(
- `ClusterIntegration|cert-manager is a native Kubernetes certificate management controller that helps with issuing certificates.
- Installing cert-manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
- are valid and up to date.`,
+ `ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
+ Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
+ are valid and up-to-date.`,
),
),
{
@@ -259,6 +262,16 @@ export default {
</span>
</div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" />
+ <p class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Point a wildcard DNS to this
+ generated IP address in order to access
+ your application after it has been deployed.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
</div>
<p v-if="!ingressExternalIp" class="settings-message js-no-ip-message">
@@ -272,17 +285,6 @@ export default {
{{ __('More information') }}
</a>
</p>
-
- <p>
- {{
- s__(`ClusterIntegration|Point a wildcard DNS to this
- generated IP address in order to access
- your application after it has been deployed.`)
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
- </p>
</template>
<div v-html="ingressDescription"></div>
</div>
@@ -295,10 +297,41 @@ export default {
:status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
+ :install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
- <div slot="description" v-html="certManagerDescription"></div>
+ <template>
+ <div slot="description">
+ <p v-html="certManagerDescription"></p>
+ <div class="form-group">
+ <label for="cert-manager-issuer-email">
+ {{ s__('ClusterIntegration|Issuer Email') }}
+ </label>
+ <div class="input-group">
+ <input
+ v-model="applications.cert_manager.email"
+ :readonly="certManagerInstalled"
+ type="text"
+ class="form-control js-email"
+ />
+ </div>
+ <p class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Issuers represent a certificate authority.
+ You must provide an email address for your Issuer. `)
+ }}
+ <a
+ href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </template>
</application-row>
<application-row
v-if="isProjectCluster"
@@ -381,16 +414,17 @@ export default {
/>
</span>
</div>
+
+ <p v-if="ingressInstalled" class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
</div>
- <p v-if="ingressInstalled">
- {{
- s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
- </p>
</template>
</div>
</application-row>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 15cf4a56138..e31afadf186 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -24,3 +24,4 @@ export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
+export const CERT_MANAGER = 'cert_manager';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 2d69da8eaec..c750daab112 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER, KNATIVE } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -30,6 +30,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ email: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
@@ -103,6 +104,9 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ } else if (appId === CERT_MANAGER) {
+ this.state.applications.cert_manager.email =
+ this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname =
serverAppEntry.hostname ||
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index c0456c18e44..952963e0711 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -192,8 +192,9 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
-export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
+export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
+ commit: state.commit,
note,
...formData,
});
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 5c6b20109bb..cbaa0e26395 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -27,6 +27,7 @@ export const getReversePosition = linePosition => {
export function getFormData(params) {
const {
+ commit,
note,
noteableType,
noteableData,
@@ -66,7 +67,7 @@ export function getFormData(params) {
position,
noteable_type: noteableType,
noteable_id: noteableData.id,
- commit_id: '',
+ commit_id: commit && commit.id,
type:
diffFile.diff_refs.start_sha && diffFile.diff_refs.head_sha
? DIFF_NOTE_TYPE
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 4a9b2903eec..3cd3b743108 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,6 +1,9 @@
<script>
+import { __ } from '~/locale';
import { GlButton } from '@gitlab/ui';
+const HIDDEN_VALUE = '••••••';
+
export default {
components: {
GlButton,
@@ -13,17 +16,26 @@ export default {
},
data() {
return {
- areVariablesVisible: false,
+ showVariableValues: false,
};
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
+ getToggleButtonText() {
+ return this.showVariableValues ? __('Hide values') : __('Reveal values');
+ },
+ hasValues() {
+ return this.trigger.variables.some(v => v.value);
+ },
},
methods: {
- revealVariables() {
- this.areVariablesVisible = true;
+ toggleValues() {
+ this.showVariableValues = !this.showVariableValues;
+ },
+ getDisplayValue(value) {
+ return this.showVariableValues ? value : HIDDEN_VALUE;
},
},
};
@@ -33,31 +45,33 @@ export default {
<div class="build-widget block">
<h4 class="title">{{ __('Trigger') }}</h4>
- <p v-if="trigger.short_token" class="js-short-token">
+ <p
+ v-if="trigger.short_token"
+ class="js-short-token"
+ :class="{ 'append-bottom-0': !hasVariables }"
+ >
<span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }}
</p>
- <p v-if="hasVariables">
- <gl-button
- v-if="!areVariablesVisible"
- type="button"
- class="btn btn-default group js-reveal-variables"
- @click="revealVariables"
- >
- {{ __('Reveal Variables') }}
- </gl-button>
- </p>
+ <template v-if="hasVariables">
+ <p class="trigger-variables-btn-container">
+ <span class="build-light-text"> {{ __('Variables:') }} </span>
- <dl v-if="areVariablesVisible" class="js-build-variables trigger-build-variables">
- <template v-for="variable in trigger.variables">
- <dt :key="`${variable.key}-variable`" class="js-build-variable trigger-build-variable">
- {{ variable.key }}
- </dt>
+ <gl-button v-if="hasValues" class="group js-reveal-variables" @click="toggleValues">
+ {{ getToggleButtonText }}
+ </gl-button>
+ </p>
- <dd :key="`${variable.key}-value`" class="js-build-value trigger-build-value">
- {{ variable.value }}
- </dd>
- </template>
- </dl>
+ <table class="js-build-variables trigger-build-variables">
+ <tr v-for="(variable, index) in trigger.variables" :key="`${variable.key}-${index}`">
+ <td class="js-build-variable trigger-build-variable trigger-variables-table-cell">
+ {{ variable.key }}
+ </td>
+ <td class="js-build-value trigger-build-value trigger-variables-table-cell">
+ {{ getDisplayValue(variable.value) }}
+ </td>
+ </tr>
+ </table>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 6f42382246d..7933c234384 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => {
};
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
+
+export const canScrollUp = ({ scrollTop }, margin = 0) => scrollTop > margin;
+
+export const canScrollDown = ({ scrollTop, offsetHeight, scrollHeight }, margin = 0) =>
+ scrollTop + offsetHeight < scrollHeight - margin;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d32f39881dd..75c18a9b6a0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -155,7 +155,7 @@ export default class MilestoneSelect {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
- let data, boardsStore;
+ let data, modalStoreFilter;
if (!selected) return;
if (options.handleClick) {
@@ -179,11 +179,11 @@ export default class MilestoneSelect {
}
if ($dropdown.closest('.add-issues-modal').length) {
- boardsStore = ModalStore.store.filter;
+ modalStoreFilter = ModalStore.store.filter;
}
- if (boardsStore) {
- boardsStore[$dropdown.data('fieldName')] = selected.name;
+ if (modalStoreFilter) {
+ modalStoreFilter[$dropdown.data('fieldName')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 841fcec96e8..ce56beb1e6b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -247,15 +247,19 @@ Please check your network connection and try again.`;
} else {
this.reopenIssue()
.then(() => this.enableButton())
- .catch(() => {
+ .catch(({ data }) => {
this.enableButton();
this.toggleStateButtonLoading(false);
- Flash(
- sprintf(
- __('Something went wrong while reopening the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- ),
+ let errorMessage = sprintf(
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
);
+
+ if (data) {
+ errorMessage = Object.values(data).join('\n');
+ }
+
+ Flash(errorMessage);
});
}
},
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index c4c8cf86cb0..e7fa05faa8a 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -12,6 +12,10 @@ export default function notificationsDropdown() {
const form = $(this).parents('.notification-form:first');
form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
+ if (form.hasClass('no-label')) {
+ form.find('.js-notification-loading').toggleClass('hidden');
+ form.find('.js-notifications-icon').toggleClass('hidden');
+ }
form.find('#notification_setting_level').val(notificationLevel);
form.submit();
});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index a6bee49a6b1..b288989b252 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -13,6 +13,9 @@ export default class Project {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) {
@@ -36,7 +39,11 @@ export default class Project {
$label.text(activeText);
});
- $projectCloneField.val(url);
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $projectCloneField.val(url);
+ }
$('.js-git-empty .js-clone').text(url);
});
// Ref switcher
diff --git a/app/assets/javascripts/terminal/index.js b/app/assets/javascripts/terminal/index.js
index 49aeb377c74..8faff59fd45 100644
--- a/app/assets/javascripts/terminal/index.js
+++ b/app/assets/javascripts/terminal/index.js
@@ -1,3 +1,3 @@
import Terminal from './terminal';
-export default () => new Terminal({ selector: '#terminal' });
+export default () => new Terminal(document.getElementById('terminal'));
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index b24aa8a3a34..560f50ebf8f 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -1,9 +1,15 @@
+import _ from 'underscore';
import $ from 'jquery';
import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
+import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+
+const SCROLL_MARGIN = 5;
+
+Terminal.applyAddon(fit);
export default class GLTerminal {
- constructor(options = {}) {
+ constructor(element, options = {}) {
this.options = Object.assign(
{},
{
@@ -13,7 +19,8 @@ export default class GLTerminal {
options,
);
- this.container = document.querySelector(options.selector);
+ this.container = element;
+ this.onDispose = [];
this.setSocketUrl();
this.createTerminal();
@@ -34,8 +41,6 @@ export default class GLTerminal {
}
createTerminal() {
- Terminal.applyAddon(fit);
-
this.terminal = new Terminal(this.options);
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
@@ -72,4 +77,48 @@ export default class GLTerminal {
handleSocketFailure() {
this.terminal.write('\r\nConnection failure');
}
+
+ addScrollListener(onScrollLimit) {
+ const viewport = this.container.querySelector('.xterm-viewport');
+ const listener = _.throttle(() => {
+ onScrollLimit({
+ canScrollUp: canScrollUp(viewport, SCROLL_MARGIN),
+ canScrollDown: canScrollDown(viewport, SCROLL_MARGIN),
+ });
+ });
+
+ this.onDispose.push(() => viewport.removeEventListener('scroll', listener));
+ viewport.addEventListener('scroll', listener);
+
+ // don't forget to initialize value before scroll!
+ listener({ target: viewport });
+ }
+
+ disable() {
+ this.terminal.setOption('cursorBlink', false);
+ this.terminal.setOption('theme', { foreground: '#707070' });
+ this.terminal.setOption('disableStdin', true);
+ this.socket.close();
+ }
+
+ dispose() {
+ this.terminal.off('data');
+ this.terminal.dispose();
+ this.socket.close();
+
+ this.onDispose.forEach(fn => fn());
+ this.onDispose.length = 0;
+ }
+
+ scrollToTop() {
+ this.terminal.scrollToTop();
+ }
+
+ scrollToBottom() {
+ this.terminal.scrollToBottom();
+ }
+
+ fit() {
+ this.terminal.fit();
+ }
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index e742900dbcb..373794fb1f2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -44,6 +44,7 @@ export default {
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
+ data-boundary="viewport"
@click="handleClick"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index fcf282a7d7c..054c75912ea 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -21,6 +21,7 @@
&.s46 { @include avatar-size(46px, 15px); }
&.s48 { @include avatar-size(48px, 10px); }
&.s60 { @include avatar-size(60px, 12px); }
+ &.s64 { @include avatar-size(64px, 14px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
@@ -80,6 +81,7 @@
&.s40 { font-size: 16px; line-height: 38px; }
&.s48 { font-size: 20px; line-height: 46px; }
&.s60 { font-size: 32px; line-height: 58px; }
+ &.s64 { font-size: 32px; line-height: 64px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 219fd99b097..e36f99ac577 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -142,8 +142,14 @@
&.btn-sm {
padding: 4px 10px;
- font-size: 13px;
- line-height: 18px;
+ font-size: $gl-btn-small-font-size;
+ line-height: $gl-btn-small-line-height;
+ }
+
+ &.btn-xs {
+ padding: 2px $gl-btn-padding;
+ font-size: $gl-btn-small-font-size;
+ line-height: $gl-btn-small-line-height;
}
&.btn-success,
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 626c8f92d1d..f2f3a45ca09 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -386,3 +386,4 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
+.min-height-0 { min-height: 0; }
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 39410ac56af..c0cda29e239 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -383,6 +383,16 @@
top: 1px;
}
}
+
+ .dropdown-menu li a .identicon {
+ width: 17px;
+ height: 17px;
+ font-size: $gl-font-size-xs;
+ vertical-align: middle;
+ text-indent: 0;
+ line-height: $gl-font-size-xs + 2px;
+ display: inline-block;
+ }
}
.breadcrumbs-list {
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 6d20c46b99d..3bb046d0e51 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -39,15 +39,6 @@
.git-clone-holder {
display: none;
}
-
- // Display Star and Fork buttons without counters on mobile.
- .project-repo-buttons {
- display: block;
-
- .count-buttons .count-badge {
- margin-top: $gl-padding-8;
- }
- }
}
.group-buttons {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4fcdb862b6d..134b3a4521b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -197,6 +197,7 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
+$gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
@@ -270,7 +271,8 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
-$project-title-row-height: 24px;
+$project-title-row-height: 64px;
+$project-avatar-mobile-size: 24px;
$gl-line-height: 16px;
$gl-line-height-24: 24px;
@@ -365,6 +367,8 @@ $gl-btn-padding: 10px;
$gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
+$gl-btn-small-font-size: 13px;
+$gl-btn-small-line-height: 13px;
/*
* Badges
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
new file mode 100644
index 00000000000..896a3466cb4
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -0,0 +1,18 @@
+@mixin ide-trace-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ margin-top: -$grid-size;
+ margin-bottom: -$grid-size;
+
+ &.build-page .top-bar {
+ top: 0;
+ height: auto;
+ font-size: 12px;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ .top-bar {
+ margin-left: -$gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 07d82e984ba..98d0a2d43ea 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,5 +1,6 @@
@import 'framework/variables';
@import 'framework/mixins';
+@import './ide_mixins';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
@@ -1111,11 +1112,7 @@ $ide-commit-header-height: 48px;
}
.ide-pipeline {
- display: flex;
- flex-direction: column;
- height: 100%;
- margin-top: -$grid-size;
- margin-bottom: -$grid-size;
+ @include ide-trace-view();
.empty-state {
margin-top: auto;
@@ -1133,17 +1130,9 @@ $ide-commit-header-height: 48px;
}
}
- .build-trace,
- .top-bar {
+ .build-trace {
margin-left: -$gl-padding;
}
-
- &.build-page .top-bar {
- top: 0;
- height: auto;
- font-size: 12px;
- border-top-right-radius: $border-radius-default;
- }
}
.ide-pipeline-list {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 81cb519883b..57918eafd6f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -228,9 +228,16 @@
padding: 16px 0;
}
+ .trigger-variables-btn-container {
+ @extend .d-flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
.trigger-build-variables {
margin: 0;
overflow-x: auto;
+ width: 100%;
-ms-overflow-style: scrollbar;
-webkit-overflow-scrolling: touch;
}
@@ -243,7 +250,15 @@
.trigger-build-value {
padding: 2px 4px;
color: $black;
- background-color: $white-light;
+ }
+
+ .trigger-variables-table-cell {
+ font-size: $gl-font-size-small;
+ line-height: $gl-line-height;
+ border: 1px solid $theme-gray-200;
+ padding: $gl-padding-4 6px;
+ width: 50%;
+ vertical-align: top;
}
.badge.badge-pill {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6cc21072acd..278800aba95 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -144,7 +144,6 @@
.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
- border-bottom: 1px solid $border-color;
.group-avatar {
float: none;
@@ -155,7 +154,6 @@
}
}
- .project-title,
.group-title {
margin-top: 10px;
margin-bottom: 10px;
@@ -195,25 +193,69 @@
}
.project-home-panel {
- padding-top: $gl-padding-8;
- padding-bottom: $gl-padding-24;
-
- .project-title-row {
- margin-right: $gl-padding-8;
- }
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
.project-avatar {
width: $project-title-row-height;
height: $project-title-row-height;
flex-shrink: 0;
flex-basis: $project-title-row-height;
- margin: 0 $gl-padding-8 0 0;
+ margin: 0 $gl-padding 0 0;
}
.project-title {
+ margin-top: 8px;
+ margin-bottom: 5px;
font-size: 20px;
- line-height: $project-title-row-height;
+ line-height: $gl-line-height-24;
font-weight: bold;
+
+ .icon {
+ font-size: $gl-font-size-large;
+ }
+
+ .project-visibility {
+ color: $gl-text-color-secondary;
+ }
+
+ .project-tag-list {
+ font-size: $gl-font-size;
+ font-weight: $gl-font-weight-normal;
+
+ .icon {
+ position: relative;
+ top: 3px;
+ margin-right: $gl-padding-4;
+ }
+ }
+ }
+
+ .project-title-row {
+ @include media-breakpoint-down(sm) {
+ .project-avatar {
+ width: $project-avatar-mobile-size;
+ height: $project-avatar-mobile-size;
+ flex-basis: $project-avatar-mobile-size;
+
+ .avatar {
+ font-size: 20px;
+ line-height: 46px;
+ }
+ }
+
+ .project-title {
+ margin-top: 4px;
+ margin-bottom: 2px;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size-large;
+ }
+
+ .project-tag-list,
+ .project-metadata {
+ font-size: $gl-font-size-small;
+ }
+ }
}
.project-metadata {
@@ -222,16 +264,6 @@
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
- .icon {
- margin-right: $gl-padding-4;
- font-size: 16px;
- }
-
- .project-visibility,
- .project-license,
- .project-tag-list {
- margin-right: $gl-padding-8;
- }
.project-license {
.btn {
@@ -240,12 +272,22 @@
}
}
- .project-tag-list,
- .project-license {
- .icon {
- position: relative;
- top: 2px;
- }
+ .access-request-link,
+ .project-tag-list {
+ padding-left: $gl-padding-8;
+ border-left: 1px solid $gl-text-color-secondary;
+ }
+ }
+
+ .project-description {
+ @include media-breakpoint-up(md) {
+ font-size: $gl-font-size-large;
+ }
+ }
+
+ .notifications-btn {
+ .fa-bell {
+ margin-right: 0;
}
}
}
@@ -298,14 +340,6 @@
vertical-align: top;
margin-top: $gl-padding;
- .count-badge {
- height: $input-height;
-
- .icon {
- top: -1px;
- }
- }
-
.count-badge-count,
.count-badge-button {
border: 1px solid $border-color;
@@ -319,29 +353,25 @@
.count-badge-count {
padding: 0 12px;
- border-right: 0;
- border-radius: $border-radius-base 0 0 $border-radius-base;
background: $gray-light;
+ border-radius: 0 $border-radius-base $border-radius-base 0;
}
.count-badge-button {
- border-radius: 0 $border-radius-base $border-radius-base 0;
+ border-right: 0;
+ border-radius: $border-radius-base 0 0 $border-radius-base;
}
}
.project-clone-holder {
display: inline-block;
- margin: $gl-padding $gl-padding-8 0 0;
+ margin: $gl-padding 0 0;
input {
height: $input-height;
}
}
- .clone-dropdown-btn {
- background-color: $white-light;
- }
-
.clone-options-dropdown {
min-width: 240px;
@@ -355,6 +385,31 @@
}
}
+.project-repo-buttons {
+ .icon {
+ top: 0;
+ }
+
+ .count-badge,
+ .btn-xs {
+ height: 24px;
+ }
+
+ .dropdown-toggle,
+ .clone-dropdown-btn {
+ .fa {
+ color: unset;
+ }
+ }
+
+ .btn {
+ .notifications-icon {
+ top: 1px;
+ margin-right: 0;
+ }
+ }
+}
+
.split-one {
display: inline-table;
margin-right: 12px;
@@ -715,10 +770,10 @@
border-bottom: 1px solid $border-color;
}
-.project-stats {
+.project-stats,
+.project-buttons {
font-size: 0;
text-align: center;
- border-bottom: 1px solid $border-color;
.scrolling-tabs-container {
.scrolling-tabs {
@@ -786,23 +841,43 @@
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
- white-space: nowrap;
+ white-space: pre-wrap;
}
.stat-link {
border-bottom: 0;
+ color: $black;
&:hover,
&:focus {
- color: $gl-text-color;
text-decoration: underline;
border-bottom: 0;
}
+
+ .project-stat-value {
+ color: $gl-text-color;
+ }
+
+ .icon {
+ color: $gl-text-color-secondary;
+ }
+
+ .add-license-link {
+ &,
+ .icon {
+ color: $blue-600;
+ }
+ }
}
.btn {
- padding: $gl-btn-vert-padding $gl-btn-horz-padding;
+ margin-top: $gl-padding;
+ padding: $gl-btn-vert-padding $gl-btn-padding;
line-height: $gl-btn-line-height;
+
+ .icon {
+ top: 0;
+ }
}
.btn-missing {
@@ -811,6 +886,13 @@
}
}
+.project-buttons {
+ .stat-text {
+ @extend .btn;
+ @extend .btn-default;
+ }
+}
+
.repository-languages-bar {
height: 8px;
margin-bottom: $gl-padding-8;
@@ -934,8 +1016,6 @@ pre.light-well {
}
.git-clone-holder {
- width: 320px;
-
.btn-clipboard {
border: 1px solid $border-color;
}
@@ -958,6 +1038,15 @@ pre.light-well {
}
}
+.git-clone-holder,
+.mobile-git-clone {
+ .btn {
+ .icon {
+ fill: $white;
+ }
+ }
+}
+
.cannot-be-merged,
.cannot-be-merged:hover {
color: $red-500;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 65c1576d9d2..7c8c1392c1c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,6 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
- include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
@@ -129,6 +128,7 @@ class ApplicationController < ActionController::Base
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
+ payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
logged_user = auth_user
@@ -155,7 +155,7 @@ class ApplicationController < ActionController::Base
end
def log_exception(exception)
- Raven.capture_exception(exception) if sentry_enabled?
+ Gitlab::Sentry.track_acceptable_exception(exception)
backtrace_cleaner = Gitlab.rails5? ? request.env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
@@ -487,4 +487,8 @@ class ApplicationController < ActionController::Base
def impersonator
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
end
+
+ def sentry_context
+ Gitlab::Sentry.context(current_user)
+ end
end
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 250f42f3096..c4e7fc950f9 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -23,6 +23,6 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def create_cluster_application_params
- params.permit(:application, :hostname)
+ params.permit(:application, :hostname, :email)
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 9aa98e2ca1f..a597996a362 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -102,7 +102,7 @@ module IssuableCollections
elsif @group
options[:group_id] = @group.id
options[:include_subgroups] = true
- options[:use_cte_for_search] = true
+ options[:attempt_group_search_optimizations] = true
end
params.permit(finder_type.valid_params).merge(options)
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 84dce74ace8..384f308269a 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -16,7 +16,11 @@ class NotificationSettingsController < ApplicationController
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
- render_response
+ if params[:hide_label].present?
+ render_response("projects/buttons/_notifications")
+ else
+ render_response
+ end
end
private
@@ -37,9 +41,9 @@ class NotificationSettingsController < ApplicationController
can?(current_user, ability_name, resource)
end
- def render_response
+ def render_response(response_template = "shared/notifications/_button")
render json: {
- html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
+ html: view_to_html_string(response_template, notification_setting: @notification_setting),
saved: @saved
}
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d521db79f85..da9316d5f22 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -122,17 +122,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
respond_to do |format|
format.html do
- if @merge_request.valid?
- redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
- else
+ if @merge_request.errors.present?
define_edit_vars
render :edit
+ else
+ redirect_to project_merge_request_path(@merge_request.target_project, @merge_request)
end
end
format.json do
- render json: serializer.represent(@merge_request, serializer: 'basic')
+ if merge_request.errors.present?
+ render json: @merge_request.errors, status: :bad_request
+ else
+ render json: serializer.represent(@merge_request, serializer: 'basic')
+ end
end
end
rescue ActiveRecord::StaleObjectError
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index e04e3a2a7e0..b73a3fa6e01 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -27,12 +27,13 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
-# use_cte_for_search: boolean
+# attempt_group_search_optimizations: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include CreatedAtFilter
+ include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
@@ -75,8 +76,9 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # This has to be last as we may use a CTE as an optimization fence by
- # passing the use_cte_for_search param
+ # This has to be last as we may use a CTE as an optimization fence
+ # by passing the attempt_group_search_optimizations param and
+ # enabling the use_cte_for_group_issues_search feature flag
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
@@ -85,6 +87,8 @@ class IssuableFinder
def filter_items(items)
items = by_project(items)
+ items = by_group(items)
+ items = by_subquery(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
@@ -282,12 +286,31 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def use_subquery_for_search?
+ strong_memoize(:use_subquery_for_search) do
+ attempt_group_search_optimizations? &&
+ Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false)
+ end
+ end
+
+ def use_cte_for_search?
+ strong_memoize(:use_cte_for_search) do
+ attempt_group_search_optimizations? &&
+ !use_subquery_for_search? &&
+ Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
+ end
+ end
+
private
def init_collection
klass.all
end
+ def attempt_group_search_optimizations?
+ search && Gitlab::Database.postgresql? && params[:attempt_group_search_optimizations]
+ end
+
def count_key(value)
Array(value).last.to_sym
end
@@ -351,12 +374,13 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def use_cte_for_search?
- return false unless search
- return false unless Gitlab::Database.postgresql?
- return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
-
- params[:use_cte_for_search]
+ # Wrap projects and groups in a subquery if the conditions are met.
+ def by_subquery(items)
+ if use_subquery_for_search?
+ klass.where(id: items.select(:id)) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ items
+ end
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 7f071d55a6b..494c754e7d5 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -85,13 +85,14 @@ module ButtonHelper
dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end
- def dropdown_item_with_description(title, description, href: nil, data: nil)
+ def dropdown_item_with_description(title, description, href: nil, data: nil, default: false)
+ active_class = "is-active" if default
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
(href ? button_content : title),
- class: "#{title.downcase}-selector",
+ class: "#{title.downcase}-selector #{active_class}",
href: (href if href),
data: (data if data)
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index b0f63de2fb8..4e11772b252 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -42,7 +42,7 @@ module IconsHelper
end
def sprite_icon(icon_name, size: nil, css_class: nil)
- if Gitlab::Sentry.should_raise?
+ if Gitlab::Sentry.should_raise_for_dev?
unless known_sprites.include?(icon_name)
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
raise exception
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
deleted file mode 100644
index d53eaef9952..00000000000
--- a/app/helpers/sentry_helper.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module SentryHelper
- def sentry_enabled?
- Gitlab::Sentry.enabled?
- end
-
- def sentry_context
- Gitlab::Sentry.context(current_user)
- end
-end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index e690350a0d1..712f0f808dd 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -140,7 +140,7 @@ module VisibilityLevelHelper
end
def project_visibility_icon_description(level)
- "#{project_visibility_level_description(level)}"
+ "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
end
def visibility_level_label(level)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d60861dc95f..d86a6eceb59 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -120,7 +120,7 @@ module Ci
acts_as_taggable
- add_authentication_token_field :token
+ add_authentication_token_field :token, encrypted: true, fallback: true
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 077e2bda143..74ef7c7e145 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -14,6 +14,10 @@ module Clusters
default_value_for :version, VERSION
+ default_value_for :email do |cert_manager|
+ cert_manager.cluster&.user&.email
+ end
+
validates :email, presence: true
def chart
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 67746e34913..c931b340b24 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.38'.freeze
+ VERSION = '0.1.39'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2c89da88b9b..a422a0995ff 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -177,7 +177,9 @@ class Commit
def title
return full_title if full_title.length < 100
- full_title.truncate(81, separator: ' ', omission: '…')
+ # Use three dots instead of the ellipsis Unicode character because
+ # some clients show the raw Unicode value in the merge commit.
+ full_title.truncate(81, separator: ' ', omission: '...')
end
# Returns the full commits title
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index c180d7b7c9a..266c37fa3a1 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -38,12 +38,13 @@ module DiscussionOnDiff
end
# Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines(highlight: true)
+ def truncated_diff_lines(highlight: true, diff_limit: nil)
return [] if diff_line.nil? && first_note.is_a?(LegacyDiffNote)
+ diff_limit = [diff_limit, NUMBER_OF_TRUNCATED_DIFF_LINES].compact.min
lines = highlight ? highlighted_diff_lines : diff_lines
- initial_line_index = [diff_line.index - NUMBER_OF_TRUNCATED_DIFF_LINES + 1, 0].max
+ initial_line_index = [diff_line.index - diff_limit + 1, 0].max
prev_lines = []
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index 2bfa7da6c1c..1e3afd641ed 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -70,13 +70,14 @@ module FastDestroyAll
module Helpers
extend ActiveSupport::Concern
+ include AfterCommitQueue
class_methods do
##
# This method is to be defined on models which have fast destroyable models as children,
# and let us avoid to use `dependent: :destroy` hook
- def use_fast_destroy(relation)
- before_destroy(prepend: true) do
+ def use_fast_destroy(relation, opts = {})
+ set_callback :destroy, :before, opts.merge(prepend: true) do
perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index 2bdef2a40e4..d79c0eae77e 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -17,6 +17,8 @@
module WithUploads
extend ActiveSupport::Concern
+ include FastDestroyAll::Helpers
+ include FeatureGate
# Currently there is no simple way how to select only not-mounted
# uploads, it should be all FileUploaders so we select them by
@@ -25,21 +27,40 @@ module WithUploads
included do
has_many :uploads, as: :model
+ has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) }, class_name: 'Upload', as: :model
- before_destroy :destroy_file_uploads
+ # TODO: when feature flag is removed, we can use just dependent: destroy
+ # option on :file_uploads
+ before_destroy :remove_file_uploads
+
+ use_fast_destroy :file_uploads, if: :fast_destroy_enabled?
+ end
+
+ def retrieve_upload(_identifier, paths)
+ uploads.find_by(path: paths)
end
+ private
+
# mounted uploads are deleted in carrierwave's after_commit hook,
# but FileUploaders which are not mounted must be deleted explicitly and
# it can not be done in after_commit because FileUploader requires loads
# associated model on destroy (which is already deleted in after_commit)
- def destroy_file_uploads
- self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload|
+ def remove_file_uploads
+ fast_destroy_enabled? ? delete_uploads : destroy_uploads
+ end
+
+ def delete_uploads
+ file_uploads.delete_all(:delete_all)
+ end
+
+ def destroy_uploads
+ file_uploads.find_each do |upload|
upload.destroy
end
end
- def retrieve_upload(_identifier, paths)
- uploads.find_by(path: paths)
+ def fast_destroy_enabled?
+ Feature.enabled?(:fast_destroy_uploads, self)
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d0811a715bc..861211ffc0a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -539,15 +539,26 @@ class MergeRequest < ActiveRecord::Base
def validate_branches
if target_project == source_project && target_branch == source_branch
- errors.add :branch_conflict, "You can not use same project/branch for source and target"
+ errors.add :branch_conflict, "You can't use same project/branch for source and target"
+ return
end
if opened?
- similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
- similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
- if similar_mrs.any?
- errors.add :validate_branches,
- "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
+ similar_mrs = target_project
+ .merge_requests
+ .where(source_branch: source_branch, target_branch: target_branch)
+ .where(source_project_id: source_project&.id)
+ .opened
+
+ similar_mrs = similar_mrs.where.not(id: id) if persisted?
+
+ conflict = similar_mrs.first
+
+ if conflict.present?
+ errors.add(
+ :validate_branches,
+ "Another open merge request already exists for this source branch: #{conflict.to_reference}"
+ )
end
end
end
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index bad0e30ceb5..dbde00b5584 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -1,12 +1,89 @@
# frozen_string_literal: true
+# The PoolRepository model is the database equivalent of an ObjectPool for Gitaly
+# That is; PoolRepository is the record in the database, ObjectPool is the
+# repository on disk
class PoolRepository < ActiveRecord::Base
include Shardable
+ include AfterCommitQueue
+
+ has_one :source_project, class_name: 'Project'
+ validates :source_project, presence: true
has_many :member_projects, class_name: 'Project'
after_create :correct_disk_path
+ state_machine :state, initial: :none do
+ state :scheduled
+ state :ready
+ state :failed
+
+ event :schedule do
+ transition none: :scheduled
+ end
+
+ event :mark_ready do
+ transition [:scheduled, :failed] => :ready
+ end
+
+ event :mark_failed do
+ transition all => :failed
+ end
+
+ state all - [:ready] do
+ def joinable?
+ false
+ end
+ end
+
+ state :ready do
+ def joinable?
+ true
+ end
+ end
+
+ after_transition none: :scheduled do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::CreateWorker.perform_async(pool.id)
+ end
+ end
+
+ after_transition scheduled: :ready do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::ScheduleJoinWorker.perform_async(pool.id)
+ end
+ end
+ end
+
+ def create_object_pool
+ object_pool.create
+ end
+
+ # The members of the pool should have fetched the missing objects to their own
+ # objects directory. If the caller fails to do so, data loss might occur
+ def delete_object_pool
+ object_pool.delete
+ end
+
+ def link_repository(repository)
+ object_pool.link(repository.raw)
+ end
+
+ # This RPC can cause data loss, as not all objects are present the local repository
+ # No execution path yet, will be added through:
+ # https://gitlab.com/gitlab-org/gitaly/issues/1415
+ def delete_repository_alternate(repository)
+ object_pool.unlink_repository(repository.raw)
+ end
+
+ def object_pool
+ @object_pool ||= Gitlab::Git::ObjectPool.new(
+ shard.name,
+ disk_path + '.git',
+ source_project.repository.raw)
+ end
+
private
def correct_disk_path
diff --git a/app/models/project.rb b/app/models/project.rb
index 9e736a3b03c..f5dc58cd67f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1585,6 +1585,7 @@ class Project < ActiveRecord::Base
import_state.remove_jid
update_project_counter_caches
after_create_default_branch
+ join_pool_repository
refresh_markdown_cache!
end
@@ -1981,8 +1982,48 @@ class Project < ActiveRecord::Base
Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
+ def object_pool_params
+ return {} unless !forked? && git_objects_poolable?
+
+ {
+ repository_storage: repository_storage,
+ pool_repository: pool_repository || create_new_pool_repository
+ }
+ end
+
+ # Git objects are only poolable when the project is or has:
+ # - Hashed storage -> The object pool will have a remote to its members, using relative paths.
+ # If the repository path changes we would have to update the remote.
+ # - Public -> User will be able to fetch Git objects that might not exist
+ # in their own repository.
+ # - Repository -> Else the disk path will be empty, and there's nothing to pool
+ def git_objects_poolable?
+ hashed_storage?(:repository) &&
+ public? &&
+ repository_exists? &&
+ Gitlab::CurrentSettings.hashed_storage_enabled &&
+ Feature.enabled?(:object_pools, self)
+ end
+
private
+ def create_new_pool_repository
+ pool = begin
+ create_or_find_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ pool.schedule
+ pool
+ end
+
+ def join_pool_repository
+ return unless pool_repository
+
+ ObjectPool::JoinWorker.perform_async(pool_repository.id, self.id)
+ end
+
def use_hashed_storage
if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
self.storage_version = LATEST_STORAGE_VERSION
diff --git a/app/models/shard.rb b/app/models/shard.rb
index 2e75bc91df0..e39d4232486 100644
--- a/app/models/shard.rb
+++ b/app/models/shard.rb
@@ -18,7 +18,9 @@ class Shard < ActiveRecord::Base
end
def self.by_name(name)
- find_or_create_by(name: name)
+ transaction(requires_new: true) do
+ find_or_create_by(name: name)
+ end
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index e01e9c6a4f0..20860f14b83 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -25,6 +25,25 @@ class Upload < ActiveRecord::Base
Digest::SHA256.file(path).hexdigest
end
+ class << self
+ ##
+ # FastDestroyAll concerns
+ def begin_fast_destroy
+ {
+ Uploads::Local => Uploads::Local.new.keys(with_files_stored_locally),
+ Uploads::Fog => Uploads::Fog.new.keys(with_files_stored_remotely)
+ }
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def finalize_fast_destroy(keys)
+ keys.each do |store_class, paths|
+ store_class.new.delete_keys_async(paths)
+ end
+ end
+ end
+
def absolute_path
raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path?
diff --git a/app/models/uploads/base.rb b/app/models/uploads/base.rb
new file mode 100644
index 00000000000..f9814159958
--- /dev/null
+++ b/app/models/uploads/base.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Base
+ BATCH_SIZE = 100
+
+ attr_reader :logger
+
+ def initialize(logger: nil)
+ @logger ||= Rails.logger
+ end
+
+ def delete_keys_async(keys_to_delete)
+ keys_to_delete.each_slice(BATCH_SIZE) do |batch|
+ DeleteStoredFilesWorker.perform_async(self.class, batch)
+ end
+ end
+ end
+end
diff --git a/app/models/uploads/fog.rb b/app/models/uploads/fog.rb
new file mode 100644
index 00000000000..b44e273e9ab
--- /dev/null
+++ b/app/models/uploads/fog.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Fog < Base
+ include ::Gitlab::Utils::StrongMemoize
+
+ def available?
+ object_store.enabled
+ end
+
+ def keys(relation)
+ return [] unless available?
+
+ relation.pluck(:path)
+ end
+
+ def delete_keys(keys)
+ keys.each do |key|
+ connection.delete_object(bucket_name, key)
+ end
+ end
+
+ private
+
+ def object_store
+ Gitlab.config.uploads.object_store
+ end
+
+ def bucket_name
+ return unless available?
+
+ object_store.remote_directory
+ end
+
+ def connection
+ return unless available?
+
+ strong_memoize(:connection) do
+ ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+ end
+ end
+end
diff --git a/app/models/uploads/local.rb b/app/models/uploads/local.rb
new file mode 100644
index 00000000000..2901c33c359
--- /dev/null
+++ b/app/models/uploads/local.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Local < Base
+ def keys(relation)
+ relation.includes(:model).find_each.map(&:absolute_path)
+ end
+
+ def delete_keys(keys)
+ keys.each do |path|
+ delete_file(path)
+ end
+ end
+
+ private
+
+ def delete_file(path)
+ unless exists?(path)
+ logger.warn("File '#{path}' doesn't exist, skipping")
+ return
+ end
+
+ unless in_uploads?(path)
+ message = "Path '#{path}' is not in uploads dir, skipping"
+ logger.warn(message)
+ Gitlab::Sentry.track_exception(RuntimeError.new(message), extra: { uploads_dir: storage_dir })
+ return
+ end
+
+ FileUtils.rm(path)
+ delete_dir!(File.dirname(path))
+ end
+
+ def exists?(path)
+ path.present? && File.exist?(path)
+ end
+
+ def in_uploads?(path)
+ path.start_with?(storage_dir)
+ end
+
+ def delete_dir!(path)
+ Dir.rmdir(path)
+ rescue Errno::ENOENT
+ # Ignore: path does not exist
+ rescue Errno::ENOTDIR
+ # Ignore: path is not a dir
+ rescue Errno::ENOTEMPTY, Errno::EEXIST
+ # Ignore: dir is not empty
+ end
+
+ def storage_dir
+ @storage_dir ||= File.realpath(Gitlab.config.uploads.storage_path)
+ end
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index d61124fa787..9bd64ea217e 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -6,27 +6,27 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include StorageHelper
include TreeHelper
+ include IconsHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :project
- AnchorData = Struct.new(:enabled, :label, :link, :class_modifier)
+ AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon)
MAX_TAGS_TO_SHOW = 3
+ def statistic_icon(icon_name = 'plus-square-o')
+ sprite_icon(icon_name, size: 16, css_class: 'icon append-right-4')
+ end
+
def statistics_anchors(show_auto_devops_callout:)
[
- readme_anchor_data,
- changelog_anchor_data,
- contribution_guide_anchor_data,
- files_anchor_data,
+ license_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- gitlab_ci_anchor_data,
- autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
- kubernetes_cluster_anchor_data
- ].compact.select { |item| item.enabled }
+ files_anchor_data
+ ].compact.select(&:is_link)
end
def statistics_buttons(show_auto_devops_callout:)
@@ -37,27 +37,28 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data,
gitlab_ci_anchor_data
- ].compact.reject { |item| item.enabled }
+ ].compact.reject(&:is_link)
end
def empty_repo_statistics_anchors
[
- files_anchor_data,
+ license_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- autodevops_anchor_data,
- kubernetes_cluster_anchor_data
- ].compact.select { |item| item.enabled }
+ files_anchor_data
+ ].compact.select { |item| item.is_link }
end
def empty_repo_statistics_buttons
[
new_file_anchor_data,
readme_anchor_data,
+ changelog_anchor_data,
+ contribution_guide_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
- ].compact.reject { |item| item.enabled }
+ ].compact.reject { |item| item.is_link }
end
def default_view
@@ -113,7 +114,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_contribution_guide_path
- add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide')
+ add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add CONTRIBUTING')
end
def add_ci_yml_path
@@ -149,32 +150,52 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def files_anchor_data
AnchorData.new(true,
- _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
+ statistic_icon('doc-code') +
+ _('%{strong_start}%{human_size}%{strong_end} Files').html_safe % {
+ human_size: storage_counter(statistics.total_repository_size),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_tree_path(project))
end
def commits_anchor_data
AnchorData.new(true,
- n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
+ statistic_icon('commit') +
+ n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
+ commit_count: number_with_delimiter(statistics.commit_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_commits_path(project, repository.root_ref))
end
def branches_anchor_data
AnchorData.new(true,
- n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
+ statistic_icon('branch') +
+ n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
+ branch_count: number_with_delimiter(repository.branch_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_branches_path(project))
end
def tags_anchor_data
AnchorData.new(true,
- n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
+ statistic_icon('label') +
+ n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
+ tag_count: number_with_delimiter(repository.tag_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_tags_path(project))
end
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false,
- _('New file'),
+ statistic_icon + _('New file'),
project_new_blob_path(project, default_branch || 'master'),
'success')
end
@@ -183,40 +204,45 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def readme_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
AnchorData.new(false,
- _('Add Readme'),
+ statistic_icon + _('Add README'),
add_readme_path)
elsif repository.readme
- AnchorData.new(true,
- _('Readme'),
- default_view != 'readme' ? readme_path : '#readme')
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('README'),
+ default_view != 'readme' ? readme_path : '#readme',
+ 'default',
+ 'doc-text')
end
end
def changelog_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
AnchorData.new(false,
- _('Add Changelog'),
+ statistic_icon + _('Add CHANGELOG'),
add_changelog_path)
elsif repository.changelog.present?
- AnchorData.new(true,
- _('Changelog'),
- changelog_path)
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CHANGELOG'),
+ changelog_path,
+ 'default')
end
end
def license_anchor_data
+ icon = statistic_icon('scale')
+
if repository.license_blob.present?
AnchorData.new(true,
- license_short_name,
+ icon + content_tag(:strong, license_short_name, class: 'project-stat-value'),
license_path)
else
if current_user && can_current_user_push_to_default_branch?
- AnchorData.new(false,
- _('Add license'),
+ AnchorData.new(true,
+ content_tag(:span, icon + _('Add license'), class: 'add-license-link d-flex'),
add_license_path)
else
- AnchorData.new(false,
- _('No license. All rights reserved'),
+ AnchorData.new(true,
+ icon + content_tag(:strong, _('No license. All rights reserved'), class: 'project-stat-value'),
nil)
end
end
@@ -225,22 +251,29 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def contribution_guide_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
AnchorData.new(false,
- _('Add Contribution guide'),
+ statistic_icon + _('Add CONTRIBUTING'),
add_contribution_guide_path)
elsif repository.contribution_guide.present?
- AnchorData.new(true,
- _('Contribution guide'),
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CONTRIBUTING'),
contribution_guide_path)
end
end
def autodevops_anchor_data(show_auto_devops_callout: false)
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
- AnchorData.new(auto_devops_enabled?,
- auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ if auto_devops_enabled?
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('Auto DevOps enabled'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'),
+ 'default')
+ else
+ AnchorData.new(false,
+ statistic_icon + _('Enable Auto DevOps'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ end
elsif auto_devops_enabled?
- AnchorData.new(true,
+ AnchorData.new(false,
_('Auto DevOps enabled'),
nil)
end
@@ -248,27 +281,32 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def kubernetes_cluster_anchor_data
if current_user && can?(current_user, :create_cluster, project)
- cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
if clusters.empty?
- cluster_link = new_project_cluster_path(project)
- end
+ AnchorData.new(false,
+ statistic_icon + _('Add Kubernetes cluster'),
+ new_project_cluster_path(project))
+ else
+ cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
- AnchorData.new(!clusters.empty?,
- clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
- cluster_link)
+ AnchorData.new(false,
+ _('Kubernetes configured'),
+ cluster_link,
+ 'default')
+ end
end
end
def gitlab_ci_anchor_data
if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled?
AnchorData.new(false,
- _('Set up CI/CD'),
+ statistic_icon + _('Set up CI/CD'),
add_ci_yml_path)
elsif repository.gitlab_ci_yml.present?
- AnchorData.new(true,
- _('CI/CD configuration'),
- ci_configuration_path)
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CI/CD configuration'),
+ ci_configuration_path,
+ 'default')
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 2bd17e58086..7b1a0be75ca 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -6,4 +6,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
+ expose :email, if: -> (e, _) { e.respond_to?(:email) }
end
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
new file mode 100644
index 00000000000..06a8db78476
--- /dev/null
+++ b/app/serializers/diff_file_base_entity.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+class DiffFileBaseEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include SubmoduleHelper
+ include DiffHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
+
+ expose :content_sha
+ expose :submodule?, as: :submodule
+
+ expose :submodule_link do |diff_file|
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
+ end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :old_path_html do |diff_file|
+ old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ old_path
+ end
+
+ expose :new_path_html do |diff_file|
+ _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ new_path
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
+
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ next unless diff_file.blob
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
+ end
+
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
+ end
+
+ expose :file_path
+ expose :old_path
+ expose :new_path
+ expose :new_file?, as: :new_file
+ expose :collapsed?, as: :collapsed
+ expose :text?, as: :text
+ expose :diff_refs
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+ expose :renamed_file?, as: :renamed_file
+ expose :deleted_file?, as: :deleted_file
+ expose :mode_changed?, as: :mode_changed
+ expose :a_mode
+ expose :b_mode
+
+ private
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
+
+ def current_user
+ request.current_user
+ end
+end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index e570039d47e..f0881829efd 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,64 +1,12 @@
# frozen_string_literal: true
-class DiffFileEntity < Grape::Entity
- include RequestAwareEntity
+class DiffFileEntity < DiffFileBaseEntity
include CommitsHelper
- include DiffHelper
- include SubmoduleHelper
- include BlobHelper
include IconsHelper
- include TreeHelper
- include ChecksCollaboration
- include Gitlab::Utils::StrongMemoize
- expose :submodule?, as: :submodule
-
- expose :submodule_link do |diff_file|
- memoized_submodule_links(diff_file).first
- end
-
- expose :submodule_tree_url do |diff_file|
- memoized_submodule_links(diff_file).last
- end
-
- expose :blob, using: BlobEntity
-
- expose :can_modify_blob do |diff_file|
- merge_request = options[:merge_request]
-
- next unless diff_file.blob
-
- if merge_request&.source_project && current_user
- can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
- else
- false
- end
- end
-
- expose :file_hash do |diff_file|
- Digest::SHA1.hexdigest(diff_file.file_path)
- end
-
- expose :file_path
expose :too_large?, as: :too_large
- expose :collapsed?, as: :collapsed
- expose :new_file?, as: :new_file
-
- expose :deleted_file?, as: :deleted_file
- expose :renamed_file?, as: :renamed_file
- expose :mode_changed?, as: :mode_changed
- expose :old_path
- expose :new_path
- expose :mode_changed?, as: :mode_changed
- expose :a_mode
- expose :b_mode
- expose :text?, as: :text
expose :added_lines
expose :removed_lines
- expose :diff_refs
- expose :content_sha
- expose :stored_externally?, as: :stored_externally
- expose :external_storage
expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
@@ -76,36 +24,6 @@ class DiffFileEntity < Grape::Entity
)
end
- expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
- options[:environment].formatted_external_url
- end
-
- expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
- options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
- end
-
- expose :old_path_html do |diff_file|
- old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- old_path
- end
-
- expose :new_path_html do |diff_file|
- _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- new_path
- end
-
- expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
- merge_request = options[:merge_request]
-
- options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
-
- next unless merge_request.source_project
-
- project_edit_blob_path(merge_request.source_project,
- tree_join(merge_request.source_branch, diff_file.new_path),
- options)
- end
-
expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
@@ -146,18 +64,4 @@ class DiffFileEntity < Grape::Entity
# Used for parallel diffs
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? }
-
- def current_user
- request.current_user
- end
-
- def memoized_submodule_links(diff_file)
- strong_memoize(:submodule_links) do
- if diff_file.submodule?
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
- else
- []
- end
- end
- end
end
diff --git a/app/serializers/discussion_diff_file_entity.rb b/app/serializers/discussion_diff_file_entity.rb
new file mode 100644
index 00000000000..419e7edf94f
--- /dev/null
+++ b/app/serializers/discussion_diff_file_entity.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class DiscussionDiffFileEntity < DiffFileBaseEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index b6786a0d597..b2d9d52bd22 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -36,7 +36,7 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
+ expose :diff_file, using: DiscussionDiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
@@ -46,19 +46,6 @@ class DiscussionEntity < Grape::Entity
expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
- expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
- diff_file = discussion.diff_file
- partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
- options[:context].render_to_string(
- partial: "projects/diffs/#{partial}",
- locals: { diff_file: diff_file,
- position: discussion.position.to_json,
- click_to_comment: false },
- layout: false,
- formats: [:html]
- )
- end
-
expose :for_commit?, as: :for_commit
expose :commit_id
diff --git a/app/serializers/trigger_variable_entity.rb b/app/serializers/trigger_variable_entity.rb
index 56203113631..4b28db42e76 100644
--- a/app/serializers/trigger_variable_entity.rb
+++ b/app/serializers/trigger_variable_entity.rb
@@ -3,5 +3,6 @@
class TriggerVariableEntity < Grape::Entity
include RequestAwareEntity
- expose :key, :value, :public
+ expose :key, :public
+ expose :value, if: ->(_, _) { can?(request.current_user, :admin_build, request.project) }
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index a89772e82dc..92c2c1b9834 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -20,7 +20,7 @@ module Clusters
end
if application.has_attribute?(:email)
- application.email = current_user.email
+ application.email = params[:email]
end
if application.respond_to?(:oauth_application)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 667b5916f38..f712b8863cd 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -58,13 +58,27 @@ module MergeRequests
.preload(:latest_merge_request_diff)
.where(target_branch: @push.branch_name).to_a
.select(&:diff_head_commit)
+ .select do |merge_request|
+ commit_ids.include?(merge_request.diff_head_sha) &&
+ merge_request.merge_request_diff.state != 'empty'
+ end
+ merge_requests = filter_merge_requests(merge_requests)
+
+ return if merge_requests.empty?
- merge_requests = merge_requests.select do |merge_request|
- commit_ids.include?(merge_request.diff_head_sha) &&
- merge_request.merge_request_diff.state != 'empty'
+ commit_analyze_enabled = Feature.enabled?(:branch_push_merge_commit_analyze, @project, default_enabled: true)
+ if commit_analyze_enabled
+ analyzer = Gitlab::BranchPushMergeCommitAnalyzer.new(
+ @commits.reverse,
+ relevant_commit_ids: merge_requests.map(&:diff_head_sha)
+ )
end
- filter_merge_requests(merge_requests).each do |merge_request|
+ merge_requests.each do |merge_request|
+ if commit_analyze_enabled
+ merge_request.merge_commit_sha = analyzer.get_merge_commit(merge_request.diff_head_sha)
+ end
+
MergeRequests::PostMergeService
.new(merge_request.target_project, @current_user)
.execute(merge_request)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 8dc0e044875..91091c4393d 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -54,6 +54,8 @@ module Projects
new_params[:avatar] = @project.avatar
end
+ new_params.merge!(@project.object_pool_params)
+
new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index a0760c2073b..6219da2c715 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,4 +1,4 @@
-.group-home-panel.text-center
+.group-home-panel.text-center.border-bottom
%div{ class: container_class }
.avatar-container.s70.group-avatar
= group_icon(@group, class: "avatar s70 avatar-tile")
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index 94bd6f96dbc..1fbae2f64ed 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -1,13 +1,18 @@
-- discussion = @note.discussion if @note.part_of_discussion?
+- note = local_assigns.fetch(:note, @note)
+- diff_limit = local_assigns.fetch(:diff_limit, nil)
+- target_url = local_assigns.fetch(:target_url, @target_url)
+- note_style = local_assigns.fetch(:note_style, "")
+
+- discussion = note.discussion if note.part_of_discussion?
- diff_discussion = discussion&.diff_discussion?
- on_image = discussion.on_image? if diff_discussion
- if discussion
- phrase_end_char = on_image ? "." : ":"
- %p.details
+ %p{ style: "color: #777777;" }
= succeed phrase_end_char do
- = link_to @note.author_name, user_url(@note.author)
+ = link_to note.author_name, user_url(note.author)
- if diff_discussion
- if discussion.new_discussion?
@@ -15,16 +20,16 @@
- else
commented on a discussion
- on #{link_to discussion.file_path, @target_url}
+ on #{link_to discussion.file_path, target_url}
- else
- if discussion.new_discussion?
started a new discussion
- else
- commented on a #{link_to 'discussion', @target_url}
+ commented on a #{link_to 'discussion', target_url}
- elsif Gitlab::CurrentSettings.email_author_in_body
%p.details
- #{link_to @note.author_name, user_url(@note.author)} commented:
+ #{link_to note.author_name, user_url(note.author)} commented:
- if diff_discussion && !on_image
= content_for :head do
@@ -32,11 +37,11 @@
%table
= render partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
+ collection: discussion.truncated_diff_lines(diff_limit: diff_limit),
as: :line,
locals: { diff_file: discussion.diff_file,
plain: true,
email: true }
-%div
- = markdown(@note.note, pipeline: :email, author: @note.author)
+%div{ style: note_style }
+ = markdown(note.note, pipeline: :email, author: note.author)
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index c319cb55e87..4bf252b6ce1 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -1,6 +1,9 @@
-<% discussion = @note.discussion if @note.part_of_discussion? -%>
+<% note = local_assigns.fetch(:note, @note) -%>
+<% diff_limit = local_assigns.fetch(:diff_limit, nil) -%>
+
+<% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%>
-<%= @note.author_name -%>
+<%= note.author_name -%>
<% if discussion.new_discussion? -%>
<%= " started a new discussion" -%>
<% else -%>
@@ -13,14 +16,14 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
-<%= "#{@note.author_name} commented:" -%>
+<%= "#{note.author_name} commented:" -%>
<% end -%>
<% if discussion&.diff_discussion? -%>
-<% discussion.truncated_diff_lines(highlight: false).each do |line| -%>
+<% discussion.truncated_diff_lines(highlight: false, diff_limit: diff_limit).each do |line| -%>
<%= "> #{line.text}\n" -%>
<% end -%>
<% end -%>
-<%= @note.note -%>
+<%= note.note -%>
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 79530e78154..22a721ee9ad 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,7 +1,9 @@
+- is_project_overview = local_assigns.fetch(:is_project_overview, false)
- commit = local_assigns.fetch(:commit) { @repository.commit }
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
#tree-holder.tree-holder.clearfix
.nav-block
@@ -10,4 +12,8 @@
- if commit
= render 'shared/commit_well', commit: commit, ref: ref, project: project
+ - if is_project_overview
+ .project-buttons.append-bottom-default
+ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index dcef4dd5b69..e191b009db2 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,83 +1,75 @@
- empty_repo = @project.empty_repo?
-- license = @project.license_anchor_data
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
.project-home-panel{ class: ("empty-project" if empty_repo) }
- .limit-container-width{ class: container_class }
- .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8
- .project-title-row.d-flex.align-items-center
- .avatar-container.project-avatar.float-none
- = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s24', width: 24, height: 24)
- %h1.project-title.d-flex.align-items-baseline.qa-project-name
- = @project.name
- .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline
- .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
- = visibility_level_label(@project.visibility_level)
- - if license.present?
- .project-license.d-inline-flex.align-items-baseline
- = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link'
- - if @project.tag_list.present?
- .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
- = sprite_icon('tag', size: 16, css_class: 'icon')
- = @project.tags_to_show
- - if @project.has_extra_tags?
- = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
+ .project-header.row.append-bottom-8
+ .project-title-row.col-md-12.col-lg-7.d-flex
+ .avatar-container.project-avatar.float-none
+ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
+ .d-flex.flex-column.flex-wrap.align-items-baseline
+ .d-inline-flex.align-items-baseline
+ %h1.project-title.qa-project-name
+ = @project.name
+ %span.project-visibility.prepend-left-8.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
+ .project-metadata.d-flex.align-items-center
+ - if can?(current_user, :read_project, @project)
+ %span.text-secondary
+ = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
+ - if current_user
+ %span.access-request-links.prepend-left-8
+ = render 'shared/members/access_request_links', source: @project
+ - if @project.tag_list.present?
+ %span.project-tag-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
+ = sprite_icon('tag', size: 16, css_class: 'icon append-right-4')
+ = @project.tags_to_show
+ - if @project.has_extra_tags?
+ = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
- .project-home-desc
- - if @project.description.present?
- .project-description
- .project-description-markdown.read-more-container
- = markdown_field(@project, :description)
- %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" }
- = _("Read more")
-
- - if can?(current_user, :read_project, @project)
- .text-secondary.prepend-top-8
- = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
-
- - if @project.forked?
- %p
- - if @project.fork_source
- #{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(@project.fork_source) do
- = fork_source_name(@project)
- - else
- - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
- = deleted_message % { project_name: fork_source_name(@project) }
-
- - if @project.badges.present?
- .project-badges.prepend-top-default.append-bottom-default
- - @project.badges.each do |badge|
- %a.append-right-8{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: 'Project badge' }>
+ .project-repo-buttons.col-md-12.col-lg-5.d-inline-flex.flex-wrap.justify-content-lg-end
+ - if current_user
+ .d-inline-flex
+ = render 'projects/buttons/notifications', notification_setting: @notification_setting, btn_class: 'btn-xs'
- .project-repo-buttons.d-inline-flex.flex-wrap
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- if can?(current_user, :download_code, @project)
- .project-clone-holder.d-inline-flex.d-sm-none
+ .project-clone-holder.d-inline-flex.d-md-none.btn-block
= render "shared/mobile_clone_panel"
- .project-clone-holder.d-none.d-sm-inline-flex
- = render "shared/clone_panel"
+ .project-clone-holder.d-none.d-md-inline-flex
+ = render "projects/buttons/clone"
- - if show_xcode_link?(@project)
- .project-action-button.project-xcode.inline
- = render "projects/buttons/xcode_link"
+ - if can?(current_user, :download_code, @project)
+ %nav.project-stats
+ .nav-links.quick-links.mt-3
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- - if current_user
- - if can?(current_user, :download_code, @project)
- .d-none.d-sm-inline-flex
- = render 'projects/buttons/download', project: @project, ref: @ref
- .d-none.d-sm-inline-flex
- = render 'projects/buttons/dropdown'
+ .project-home-desc.mt-1
+ - if @project.description.present?
+ .project-description
+ .project-description-markdown.read-more-container
+ = markdown_field(@project, :description)
+ %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
+ = _("Read more")
+
+ - if @project.forked?
+ %p
+ - if @project.fork_source
+ #{ s_('ForkedFromProjectPath|Forked from') }
+ = link_to project_path(@project.fork_source) do
+ = fork_source_name(@project)
+ - else
+ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
+ = deleted_message % { project_name: fork_source_name(@project) }
- .d-none.d-sm-inline-flex
- = render 'shared/notifications/button', notification_setting: @notification_setting
- .d-none.d-sm-inline-flex
- = render 'shared/members/access_request_buttons', source: @project
+ - if @project.badges.present?
+ .project-badges.mb-2
+ - @project.badges.each do |badge|
+ %a.append-right-8{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 4cf49f3cf62..8e3d759b683 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -4,5 +4,5 @@
%ul.nav
- anchors.each do |anchor|
%li.nav-item
- = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
- .stat-text= anchor.label
+ = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.is_link ? 'nav-link stat-link d-flex align-items-center' : "nav-link btn btn-#{anchor.class_modifier || 'missing'} d-flex align-items-center" do
+ .stat-text.d-flex.align-items-center= anchor.label
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
new file mode 100644
index 00000000000..d82a3dd70f9
--- /dev/null
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -0,0 +1,31 @@
+- project = project || @project
+
+.git-clone-holder.js-git-clone-holder.input-group
+ - if allowed_protocols_present?
+ .input-group-text.clone-dropdown-btn.btn
+ %span.js-clone-dropdown-label
+ = enabled_project_button(project, enabled_protocol)
+ - else
+ %a#clone-dropdown.input-group-text.btn.btn-primary.btn-xs.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %span.append-right-4.js-clone-dropdown-label
+ = _('Clone')
+ = sprite_icon("arrow-down", css_class: "icon")
+ %form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown
+ %li.pb-2
+ %label.label-bold
+ = _('Clone with SSH')
+ .input-group
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
+ .input-group-append
+ = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+ = render_if_exists 'projects/buttons/geo'
+ %li
+ %label.label-bold
+ = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
+ .input-group
+ = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
+ .input-group-append
+ = clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+ = render_if_exists 'projects/buttons/geo'
+
+= render_if_exists 'shared/geo_info_modal', project: project
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index f7551434d47..4eb53faa6ff 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -5,8 +5,8 @@
.project-action-button.dropdown.inline>
%button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' }
= sprite_icon('download')
- = icon("caret-down")
%span.sr-only= _('Select Archive Format')
+ = sprite_icon("arrow-down")
%ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li.dropdown-header
#{ _('Source code') }
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 8da27ca7cb3..bc0a89bea62 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,9 +1,6 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.fork-count.count-badge-count.d-flex.align-items-center
- = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
- = @project.forks_count
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do
= sprite_icon('fork', { css_class: 'icon' })
@@ -15,3 +12,6 @@
title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
= sprite_icon('fork', { css_class: 'icon' })
%span= s_('ProjectOverview|Fork')
+ %span.fork-count.count-badge-count.d-flex.align-items-center
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
+ = @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
new file mode 100644
index 00000000000..745983ace7e
--- /dev/null
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -0,0 +1,27 @@
+- btn_class = local_assigns.fetch(:btn_class, "btn-xs")
+
+- if notification_setting
+ .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ = hidden_field_tag "hide_label", true
+ = f.hidden_field :level, class: "notification_setting_level"
+ .js-notification-toggle-btns
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ = sprite_icon("arrow-down", css_class: "icon")
+ .sr-only Toggle dropdown
+ - else
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ = sprite_icon("arrow-down", css_class: "icon")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
+
+ = content_for :scripts_body do
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 0d04ecb3a58..090d1549aa7 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,19 +1,19 @@
- if current_user
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.star-count.count-badge-count.d-flex.align-items-center
- = @project.star_count
- %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
+ %button.count-badge-button.btn.btn-default.btn-xs.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
- if current_user.starred?(@project)
= sprite_icon('star', { css_class: 'icon' })
%span.starred= s_('ProjectOverview|Unstar')
- else
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
+ %span.star-count.count-badge-count.d-flex.align-items-center
+ = @project.star_count
- else
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.star-count.count-badge-count.d-flex.align-items-center
- = @project.star_count
- = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ = link_to new_user_session_path, class: 'btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
+ %span.star-count.count-badge-count.d-flex.align-items-center
+ = @project.star_count
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 936900a0087..aa690b12eb7 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -4,11 +4,10 @@
= render partial: 'flash_messages', locals: { project: @project }
-= render "home_panel"
+%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ = render "home_panel"
-.project-empty-note-panel
- %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .prepend-top-20
+ .project-empty-note-panel
%h4.append-bottom-20
= _('The repository for this project is empty')
@@ -32,66 +31,65 @@
= _('Otherwise it is recommended you start with one of the options below.')
.prepend-top-20
-%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.quick-links
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+ %nav.project-buttons
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs.quick-links
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
-- if can?(current_user, :push_code, @project)
- %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .prepend-top-20
- .empty_wrapper
- %h3#repo-command-line-instructions.page-title-empty
- Command line instructions
- .git-empty.js-git-empty
- %fieldset
- %h5 Git global setup
- %pre.bg-light
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+ - if can?(current_user, :push_code, @project)
+ %div
+ .prepend-top-20
+ .empty_wrapper
+ %h3#repo-command-line-instructions.page-title-empty
+ = _('Command line instructions')
+ .git-empty.js-git-empty
+ %fieldset
+ %h5= _('Git global setup')
+ %pre.bg-light
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.bg-light
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ %fieldset
+ %h5= _('Create a new repository')
+ %pre.bg-light
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- %fieldset
- %h5 Existing folder
- %pre.bg-light
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- git add .
- git commit -m "Initial commit"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ %fieldset
+ %h5= _('Existing folder')
+ %pre.bg-light
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- %fieldset
- %h5 Existing Git repository
- %pre.bg-light
- :preserve
- cd existing_repo
- git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin --all
- git push -u origin --tags
+ %fieldset
+ %h5= _('Existing Git repository')
+ %pre.bg-light
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to _('Remove project'), [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 2c6484c2c99..56b06374d6d 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -5,7 +5,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-- if @labels.present? && can_admin_label
+- if labels_or_filters && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f29ce4f5c06..c87a084740b 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
-- show_auto_devops_callout = show_auto_devops_callout?(@project)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
@@ -15,20 +14,11 @@
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render "projects/last_push"
-= render "home_panel"
-
-- if can?(current_user, :download_code, @project)
- %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.quick-links
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+ = render "home_panel"
+ - if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages)
-%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
@@ -41,4 +31,4 @@
= render 'shared/auto_devops_callout'
%div{ class: project_child_container_class(view_path) }
- = render view_path
+ = render view_path, is_project_overview: true
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 601e3f25852..a89df6adfb3 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -85,4 +85,8 @@
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index 998985cabe1..b43662947a8 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -1,13 +1,13 @@
- project = project || @project
- ssh_copy_label = _("Copy SSH clone URL")
-- http_copy_label = _("Copy HTTPS clone URL")
+- http_copy_label = _('Copy %{http_label} clone URL') % { http_label: gitlab_config.protocol.upcase }
-.btn-group.mobile-git-clone.js-mobile-git-clone
- = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default")
- %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
- = icon("caret-down", class: "dropdown-btn-icon")
+.btn-group.mobile-git-clone.js-mobile-git-clone.btn-block
+ = clipboard_button(button_text: default_clone_label, text: default_url_to_repo(project), hide_button_icon: true, class: "btn-primary flex-fill bold justify-content-center input-group-text clone-dropdown-btn js-clone-dropdown-label")
+ %button.btn.btn-primary.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
+ = sprite_icon("arrow-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
%li
- = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' })
+ = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }, default: true)
%li
= dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
new file mode 100644
index 00000000000..f7227b9101e
--- /dev/null
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -0,0 +1,17 @@
+- model_name = source.model_name.to_s.downcase
+
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord
+ - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
+ = link_to link_text, polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'access-request-link'
+- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
+ = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'access-request-link'
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'access-request-link'
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index f6c7ca70ebd..30860988bbb 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,3 +1,5 @@
+- btn_class = local_assigns.fetch(:btn_class, nil)
+
- if notification_setting
.js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
@@ -6,14 +8,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 2c55806a286..dfce00a10a1 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -10,7 +10,6 @@
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
- cronjob:remove_expired_members
-- cronjob:remove_old_web_hook_logs
- cronjob:remove_unreferenced_lfs_objects
- cronjob:repository_archive_cache
- cronjob:repository_check_dispatch
@@ -86,6 +85,10 @@
- todos_destroyer:todos_destroyer_project_private
- todos_destroyer:todos_destroyer_private_features
+- object_pool:object_pool_create
+- object_pool:object_pool_schedule_join
+- object_pool:object_pool_join
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
@@ -134,3 +137,4 @@
- delete_diff_files
- detect_repository_languages
- repository_cleanup
+- delete_stored_files
diff --git a/app/workers/concerns/object_pool_queue.rb b/app/workers/concerns/object_pool_queue.rb
new file mode 100644
index 00000000000..5b648df9c72
--- /dev/null
+++ b/app/workers/concerns/object_pool_queue.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# Concern for setting Sidekiq settings for the various ObjectPool queues
+#
+module ObjectPoolQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :object_pool
+ end
+end
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
new file mode 100644
index 00000000000..ff7931849d8
--- /dev/null
+++ b/app/workers/delete_stored_files_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class DeleteStoredFilesWorker
+ include ApplicationWorker
+
+ def perform(class_name, keys)
+ klass = begin
+ class_name.constantize
+ rescue NameError
+ nil
+ end
+
+ unless klass
+ message = "Unknown class '#{class_name}'"
+ logger.error(message)
+ Gitlab::Sentry.track_exception(RuntimeError.new(message))
+ return
+ end
+
+ klass.new(logger: logger).delete_keys(keys)
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 2d381c6fd6c..d3628b23189 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -28,6 +28,8 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
+ project.repository.expire_statistics_caches
+
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
project.cleanup
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 42f5b945a75..98f9f45e608 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -8,11 +8,18 @@ class NewNoteWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note)
+ NotificationService.new.new_note(note) unless skip_notification?(note)
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
+
+ private
+
+ # EE-only method
+ def skip_notification?(note)
+ false
+ end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/object_pool/create_worker.rb b/app/workers/object_pool/create_worker.rb
new file mode 100644
index 00000000000..135b99886dc
--- /dev/null
+++ b/app/workers/object_pool/create_worker.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class CreateWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+ include ExclusiveLeaseGuard
+
+ attr_reader :pool
+
+ def perform(pool_id)
+ @pool = PoolRepository.find_by_id(pool_id)
+ return unless pool
+
+ try_obtain_lease do
+ perform_pool_creation
+ end
+ end
+
+ private
+
+ def perform_pool_creation
+ return unless pool.failed? || pool.scheduled?
+
+ # If this is a retry and the previous execution failed, deletion will
+ # bring the pool back to a pristine state
+ pool.delete_object_pool if pool.failed?
+
+ pool.create_object_pool
+ pool.mark_ready
+ rescue => e
+ pool.mark_failed
+ raise e
+ end
+
+ def lease_key
+ "object_pool:create:#{pool.id}"
+ end
+
+ def lease_timeout
+ 1.hour
+ end
+ end
+end
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
new file mode 100644
index 00000000000..07676011b2a
--- /dev/null
+++ b/app/workers/object_pool/join_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class JoinWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_id, project_id)
+ pool = PoolRepository.find_by_id(pool_id)
+ return unless pool&.joinable?
+
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ pool.link_repository(project.repository)
+
+ Projects::HousekeepingService.new(project).execute
+ end
+ end
+end
diff --git a/app/workers/object_pool/schedule_join_worker.rb b/app/workers/object_pool/schedule_join_worker.rb
new file mode 100644
index 00000000000..647a8b72435
--- /dev/null
+++ b/app/workers/object_pool/schedule_join_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class ScheduleJoinWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_id)
+ pool = PoolRepository.find_by_id(pool_id)
+ return unless pool&.joinable?
+
+ pool.member_projects.find_each do |project|
+ next if project.forked? && !project.import_finished?
+
+ ObjectPool::JoinWorker.perform_async(pool.id, project.id)
+ end
+ end
+ end
+end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
deleted file mode 100644
index 0f486f8991d..00000000000
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class RemoveOldWebHookLogsWorker
- include ApplicationWorker
- include CronjobQueue
-
- WEB_HOOK_LOG_LIFETIME = 2.days
-
- # rubocop: disable DestroyAll
- def perform
- WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
- end
- # rubocop: enable DestroyAll
-end
diff --git a/changelogs/unreleased/20422-hide-ui-variables-by-default.yml b/changelogs/unreleased/20422-hide-ui-variables-by-default.yml
new file mode 100644
index 00000000000..60285d49718
--- /dev/null
+++ b/changelogs/unreleased/20422-hide-ui-variables-by-default.yml
@@ -0,0 +1,6 @@
+---
+title: Pipeline trigger variable values are hidden in the UI by default. Maintainers
+ have the option to reveal them.
+merge_request: 23518
+author: jhampton
+type: added
diff --git a/changelogs/unreleased/22548-reopen-error-message.yml b/changelogs/unreleased/22548-reopen-error-message.yml
new file mode 100644
index 00000000000..79c20eccb12
--- /dev/null
+++ b/changelogs/unreleased/22548-reopen-error-message.yml
@@ -0,0 +1,6 @@
+---
+title: Show error message when attempting to reopen an MR and there is an open MR
+ for the same branch
+merge_request: 16447
+author: Akos Gyimesi
+type: fixed
diff --git a/changelogs/unreleased/48889-populate-merge_commit_sha.yml b/changelogs/unreleased/48889-populate-merge_commit_sha.yml
new file mode 100644
index 00000000000..0e25d8ecfb0
--- /dev/null
+++ b/changelogs/unreleased/48889-populate-merge_commit_sha.yml
@@ -0,0 +1,6 @@
+---
+title: Fix "merged with [commit]" info for merge requests being merged automatically
+ by other actions
+merge_request: 22794
+author:
+type: fixed
diff --git a/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml b/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml
new file mode 100644
index 00000000000..f695d5aeff8
--- /dev/null
+++ b/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml
@@ -0,0 +1,5 @@
+---
+title: "Make auto-generated icons for subgroups in the breadcrumb dropdown display as a circle"
+merge_request: 23062
+author: Thomas Pathier
+type: fix \ No newline at end of file
diff --git a/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml b/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml
new file mode 100644
index 00000000000..ddb5eaa89d0
--- /dev/null
+++ b/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Design improvements to project overview page
+merge_request: 22196
+author:
+type: changed
diff --git a/changelogs/unreleased/52007-frontmatter-toml-json.yml b/changelogs/unreleased/52007-frontmatter-toml-json.yml
new file mode 100644
index 00000000000..bdada19f3a7
--- /dev/null
+++ b/changelogs/unreleased/52007-frontmatter-toml-json.yml
@@ -0,0 +1,5 @@
+---
+title: Changed frontmatter filtering to support YAML, JSON, TOML, and arbitrary languages
+merge_request: 23331
+author: Travis Miller
+type: changed
diff --git a/changelogs/unreleased/cert-manager-email.yml b/changelogs/unreleased/cert-manager-email.yml
new file mode 100644
index 00000000000..530608d9660
--- /dev/null
+++ b/changelogs/unreleased/cert-manager-email.yml
@@ -0,0 +1,5 @@
+---
+title: Ability to override email for cert-manager
+merge_request: 23503
+author: Amit Rathi
+type: added
diff --git a/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml b/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml
new file mode 100644
index 00000000000..fb0c508400c
--- /dev/null
+++ b/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Remove old webhook logs after 90 days, as documented, instead of after 2
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
new file mode 100644
index 00000000000..04fc88bc3d3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
@@ -0,0 +1,5 @@
+---
+title: Encrypt CI/CD builds authentication tokens
+merge_request: 23436
+author:
+type: security
diff --git a/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml b/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml
new file mode 100644
index 00000000000..eed31950a76
--- /dev/null
+++ b/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml
@@ -0,0 +1,5 @@
+---
+title: Show primary button when all labels are prioritized
+merge_request: 23648
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml b/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml
new file mode 100644
index 00000000000..0269e7b6196
--- /dev/null
+++ b/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error when searching for group issues with priority or popularity sort
+merge_request: 23445
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml b/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml
new file mode 100644
index 00000000000..58d9a19d038
--- /dev/null
+++ b/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused data from discussions endpoint
+merge_request: 23570
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-truncate-with-periods.yml b/changelogs/unreleased/sh-truncate-with-periods.yml
new file mode 100644
index 00000000000..b1c6b4f9cbd
--- /dev/null
+++ b/changelogs/unreleased/sh-truncate-with-periods.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate merge request titles with periods instead of ellipsis
+merge_request: 23558
+author:
+type: changed
diff --git a/changelogs/unreleased/store-correlation-logs.yml b/changelogs/unreleased/store-correlation-logs.yml
new file mode 100644
index 00000000000..d5f6c789a17
--- /dev/null
+++ b/changelogs/unreleased/store-correlation-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Log and pass correlation-id between Unicorn, Sidekiq and Gitaly
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml b/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml
new file mode 100644
index 00000000000..90a5c8c4e2c
--- /dev/null
+++ b/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Fill project_repositories for hashed storage projects
+merge_request: 23482
+author:
+type: added
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml
new file mode 100644
index 00000000000..dffcdb0bb5a
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml
@@ -0,0 +1,5 @@
+---
+title: Update used version of Runner Helm Chart to 0.1.39
+merge_request: 23633
+author:
+type: other
diff --git a/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml b/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml
new file mode 100644
index 00000000000..2ce16a2b6b7
--- /dev/null
+++ b/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml
@@ -0,0 +1,5 @@
+---
+title: Pass commit when posting diff discussions
+merge_request: 23371
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-milestone-select.yml b/changelogs/unreleased/winh-milestone-select.yml
new file mode 100644
index 00000000000..8464fc6c541
--- /dev/null
+++ b/changelogs/unreleased/winh-milestone-select.yml
@@ -0,0 +1,5 @@
+---
+title: Fix milestone select in issue sidebar of issue boards
+merge_request: 23625
+author:
+type: fixed
diff --git a/changelogs/unreleased/zj-pool-repository-creation.yml b/changelogs/unreleased/zj-pool-repository-creation.yml
new file mode 100644
index 00000000000..a24b96e4924
--- /dev/null
+++ b/changelogs/unreleased/zj-pool-repository-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Allow public forks to be deduplicated
+merge_request: 23508
+author:
+type: added
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 82e3b490378..db35fa96ea2 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -302,10 +302,6 @@ Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_for_usage_ping)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
-Settings.cron_jobs['remove_old_web_hook_logs_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['remove_old_web_hook_logs_worker']['cron'] ||= '40 0 * * *'
-Settings.cron_jobs['remove_old_web_hook_logs_worker']['job_class'] = 'RemoveOldWebHookLogsWorker'
-
Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *'
Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker'
diff --git a/config/initializers/correlation_id.rb b/config/initializers/correlation_id.rb
new file mode 100644
index 00000000000..2a7c138dc40
--- /dev/null
+++ b/config/initializers/correlation_id.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+Rails.application.config.middleware.use(Gitlab::Middleware::CorrelationId)
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 840404e0ec0..c897bc30e76 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -29,6 +29,7 @@ unless Sidekiq.server?
gitaly_calls = Gitlab::GitalyClient.get_request_count
payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0
payload[:response] = event.payload[:response] if event.payload[:response]
+ payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
payload
end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 17d09293205..2a6c5148f71 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -24,4 +24,4 @@ def configure_sentry
end
end
-configure_sentry if Rails.env.production?
+configure_sentry if Rails.env.production? || Rails.env.development?
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f20ea488d9c..6aba6c7c21d 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -21,6 +21,7 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::Shutdown
chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
chain.add Gitlab::SidekiqMiddleware::BatchLoader
+ chain.add Gitlab::SidekiqMiddleware::CorrelationLogger
chain.add Gitlab::SidekiqStatus::ServerMiddleware
end
@@ -31,6 +32,7 @@ Sidekiq.configure_server do |config|
config.client_middleware do |chain|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
+ chain.add Gitlab::SidekiqMiddleware::CorrelationInjector
end
config.on :startup do
@@ -75,6 +77,7 @@ Sidekiq.configure_client do |config|
config.redis = queues_config_hash
config.client_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::CorrelationInjector
chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d8002815bac..5985569bef4 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -81,4 +81,6 @@
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
- [auto_devops, 2]
+ - [object_pool, 1]
- [repository_cleanup, 1]
+ - [delete_stored_files, 1]
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 87c61d6e90d..be7b301866d 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -24,23 +24,24 @@ The following files require a review from the Documentation team:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-When your content is ready for review, mention a technical writer in a separate
-comment and explain what needs to be reviewed.
-
-You are welcome to mention them sooner if you have questions about writing or updating
-the documentation. GitLabbers are also welcome to use the [#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.
-
-Who to ping [based on DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages):
+When your content is ready for review, assign the MR to a technical writer
+according to the [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages)
+in the table below. If necessary, mention them in a comment explaining what needs
+to be reviewed.
| Tech writer | Stage(s) |
| ------------ | ------------------------------------------------------------ |
-| `@marcia` | ~Create ~Release |
+| `@marcia` | ~Create ~Release + ~"development guidelines" |
| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Packaging ~Secure |
| `@eread` | ~Manage ~Configure ~Geo ~Verify |
| `@mikelewis` | ~Plan |
+You are welcome to mention them sooner if you have questions about writing or
+updating the documentation. GitLabbers are also welcome to use the
+[#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.
+
If you are not sure which category the change falls within, or the change is not
-part of one of these categories, you can mention one of the usernames above.
+part of one of these categories, mention one of the usernames above.
MARKDOWN
unless gitlab.mr_labels.include?('Documentation')
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 089de211380..aa8686ac7d8 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -71,13 +71,17 @@ Sidekiq::Testing.inline! do
params[:storage_version] = Project::LATEST_STORAGE_VERSION
end
- project = Projects::CreateService.new(User.first, params).execute
- # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
- # hook won't run until after the fixture is loaded. That is too late
- # since the Sidekiq::Testing block has already exited. Force clearing
- # the `after_commit` queue to ensure the job is run now.
+ project = nil
+
Sidekiq::Worker.skipping_transaction_check do
+ project = Projects::CreateService.new(User.first, params).execute
+
+ # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
+ # hook won't run until after the fixture is loaded. That is too late
+ # since the Sidekiq::Testing block has already exited. Force clearing
+ # the `after_commit` queue to ensure the job is run now.
project.send(:_run_after_commit_queue)
+ project.import_state.send(:_run_after_commit_queue)
end
if project.valid? && project.valid_repo?
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index bcfdd058a1c..8bdc7c6556c 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -25,7 +25,9 @@ Gitlab::Seeder.quiet do
developer = project.team.developers.sample
break unless developer
- MergeRequests::CreateService.new(project, developer, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, developer, params).execute
+ end
print '.'
end
end
@@ -39,7 +41,9 @@ Gitlab::Seeder.quiet do
target_branch: 'master',
title: 'Can be automatically merged'
}
- MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ end
print '.'
params = {
@@ -47,6 +51,8 @@ Gitlab::Seeder.quiet do
target_branch: 'feature',
title: 'Cannot be automatically merged'
}
- MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ end
print '.'
end
diff --git a/db/migrate/20181128123704_add_state_to_pool_repository.rb b/db/migrate/20181128123704_add_state_to_pool_repository.rb
new file mode 100644
index 00000000000..714232ede56
--- /dev/null
+++ b/db/migrate/20181128123704_add_state_to_pool_repository.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddStateToPoolRepository < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # Given the table is empty, and the non concurrent methods are chosen so
+ # the transactions don't have to be disabled
+ # rubocop: disable Migration/AddConcurrentForeignKey, Migration/AddIndex
+ def change
+ add_column(:pool_repositories, :state, :string, null: true)
+
+ add_column :pool_repositories, :source_project_id, :integer
+ add_index :pool_repositories, :source_project_id, unique: true
+ add_foreign_key :pool_repositories, :projects, column: :source_project_id, on_delete: :nullify
+ end
+ # rubocop: enable Migration/AddConcurrentForeignKey, Migration/AddIndex
+end
diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
new file mode 100644
index 00000000000..11b98203793
--- /dev/null
+++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :token_encrypted, :string
+ end
+end
diff --git a/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
new file mode 100644
index 00000000000..f90aca008e5
--- /dev/null
+++ b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiBuildsTokenEncrypted < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :token_encrypted, unique: true, where: 'token_encrypted IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :token_encrypted
+ end
+end
diff --git a/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb b/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb
new file mode 100644
index 00000000000..7814cdba58a
--- /dev/null
+++ b/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillHashedProjectRepositories < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ DELAY_INTERVAL = 5.minutes
+ MIGRATION = 'BackfillHashedProjectRepositories'
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Project, MIGRATION, DELAY_INTERVAL)
+ end
+
+ def down
+ # no-op: since there could have been existing rows before the migration do not remove anything
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fc73d30fb1f..ff2dde3243c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -345,6 +345,7 @@ ActiveRecord::Schema.define(version: 20181203002526) do
t.boolean "protected"
t.integer "failure_reason"
t.datetime_with_timezone "scheduled_at"
+ t.string "token_encrypted"
t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
t.index ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -361,6 +362,7 @@ ActiveRecord::Schema.define(version: 20181203002526) do
t.index ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
t.index ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
t.index ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
+ t.index ["token_encrypted"], name: "index_ci_builds_on_token_encrypted", unique: true, where: "(token_encrypted IS NOT NULL)", using: :btree
t.index ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
@@ -1510,8 +1512,11 @@ ActiveRecord::Schema.define(version: 20181203002526) do
create_table "pool_repositories", id: :bigserial, force: :cascade do |t|
t.integer "shard_id", null: false
t.string "disk_path"
+ t.string "state"
+ t.integer "source_project_id"
t.index ["disk_path"], name: "index_pool_repositories_on_disk_path", unique: true, using: :btree
t.index ["shard_id"], name: "index_pool_repositories_on_shard_id", using: :btree
+ t.index ["source_project_id"], name: "index_pool_repositories_on_source_project_id", unique: true, using: :btree
end
create_table "programming_languages", force: :cascade do |t|
@@ -2391,6 +2396,7 @@ ActiveRecord::Schema.define(version: 20181203002526) do
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
add_foreign_key "pool_repositories", "shards", on_delete: :restrict
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 9379944b250..12238ba7b32 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -94,6 +94,23 @@ need to be performed on these nodes as well. Database changes will propagate wit
You must make sure the migration event was already processed or otherwise it may migrate
the files back to Hashed state again.
+#### Hashed object pools
+
+For deduplication of public forks and their parent repository, objects are pooled
+in an object pool. These object pools are a third repository where shared objects
+are stored.
+
+```ruby
+# object pool paths
+"@pools/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git"
+```
+
+The object pool feature is behind the `object_pools` feature flag, and can be
+enabled for individual projects by executing
+`Feature.enable(:object_pools, Project.find(<id>))`. Note that the project has to
+be on hashed storage, should not be a fork itself, and hashed storage should be
+enabled for all new projects.
+
##### Attachments
To rollback single Attachment migration, rename `aa/bb/abcdef1234567890...` folder back to `namespace/project`.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index fc03cf6cc39..9ff6c73b1b6 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -974,10 +974,9 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git
Merge changes submitted with MR using this API.
+If merge request is unable to be accepted (ie: Work in Progress, Closed, Pipeline Pending Completion, or Failed while requiring Success) - you'll get a `405` and the error message 'Method Not Allowed'
-If it has some conflicts and can not be merged - you'll get a `405` and the error message 'Branch cannot be merged'
-
-If merge request is already merged or closed - you'll get a `406` and the error message 'Method Not Allowed'
+If it has some conflicts and can not be merged - you'll get a `406` and the error message 'Branch cannot be merged'
If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a `409` and the error message 'SHA does not match HEAD of source branch'
diff --git a/doc/api/search.md b/doc/api/search.md
index a9369930003..7e3ae7404a3 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -722,16 +722,22 @@ Example response:
### Scope: wiki_blobs
+Filters are available for this scope:
+
+- filename
+- path
+- extension
+
+To use a filter simply include it in your query like: `a query filename:some_name*`.
+You may use wildcards (`*`) to use glob matching.
+
Wiki blobs searches are performed on both filenames and contents. Search
results:
- Found in filenames are displayed before results found in contents.
- May contain multiple matches for the same blob because the search string
- might be found in both the filename and content, and matches of the different
-types are displayed separately.
-- May contain multiple matches for the same blob because the search string
- might be found if the search string appears multiple times in the content.
-
+ might be found in both the filename and content, or might appear multiple
+ times in the content.
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
@@ -788,22 +794,20 @@ Example response:
### Scope: blobs
Filters are available for this scope:
+
- filename
- path
- extension
-to use a filter simply include it in your query like so: `a query filename:some_name*`.
+To use a filter simply include it in your query like: `a query filename:some_name*`.
+You may use wildcards (`*`) to use glob matching.
Blobs searches are performed on both filenames and contents. Search results:
- Found in filenames are displayed before results found in contents.
- May contain multiple matches for the same blob because the search string
- might be found in both the filename and content, and matches of the different
-types are displayed separately.
-- May contain multiple matches for the same blob because the search string
- might be found if the search string appears multiple times in the content.
-
-You may use wildcards (`*`) to use glob matching.
+ might be found in both the filename and content, or might appear multiple
+ times in the content.
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
diff --git a/doc/ci/README.md b/doc/ci/README.md
index dba1f38abe2..4e066a0df97 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -71,6 +71,7 @@ learn how to leverage its potential even more.
- [Caching dependencies](caching/index.md)
- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
submodules are involved
+- [Pipelines for merge requests](merge_request_pipelines/index.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
diff --git a/doc/ci/merge_request_pipelines/img/merge_request.png b/doc/ci/merge_request_pipelines/img/merge_request.png
new file mode 100644
index 00000000000..1fe2eec2008
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/pipeline_detail.png b/doc/ci/merge_request_pipelines/img/pipeline_detail.png
new file mode 100644
index 00000000000..def1781dd75
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/pipeline_detail.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
new file mode 100644
index 00000000000..706e83abf44
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -0,0 +1,84 @@
+# Pipelines for merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6
+
+Usually, when a developer creates a new merge request, a pipeline runs on the
+new change and checks if it's qualified to be merged into a target branch. This
+pipeline should contain only necessary jobs for checking the new changes.
+For example, unit tests, lint checks, and Review Apps are often used in this cycle.
+
+With pipelines for merge requests, you can design a specific pipeline structure
+for merge requests. All you need to do is just adding `only: [merge_requests]` to
+the jobs that you want it to run for only merge requests.
+Every time, when developers create or update merge requests, a pipeline runs on
+their new commits at every push to GitLab.
+
+NOTE: **Note**:
+If you use both this feature and the [Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md)
+feature, pipelines for merge requests take precendence over the other regular pipelines.
+
+For example, consider a GitLab CI/CD configuration in .gitlab-ci.yml as follows:
+
+```yaml
+build:
+ stage: build
+ script: ./build
+ only:
+ - branches
+ - tags
+ - merge_requests
+
+test:
+ stage: test
+ script: ./test
+ only:
+ - merge_requests
+
+deploy:
+ stage: deploy
+ script: ./deploy
+```
+
+After a developer updated code in a merge request with whatever methods (e.g. `git push`),
+GitLab detects that the code is updated and create a new pipeline for the merge request.
+The pipeline fetches the latest code from the source branch and run tests against it.
+In this example, the pipeline contains only `build` and `test` jobs.
+Since `deploy` job does not have the `only: [merge_requests]` rule,
+deployment jobs will not happen in the merge request.
+
+Consider this pipeline list viewed from the **Pipelines** tab in a merge request:
+
+![Merge request page](img/merge_request.png)
+
+Note that pipelines tagged as **merge request** indicate that they were triggered
+when a merge request was created or updated.
+
+The same tag is shown on the pipeline's details:
+
+![Pipeline's details](img/pipeline_detail.png)
+
+## Important notes about merge requests from forked projects
+
+Note that the current behavior is subject to change. In the usual contribution
+flow, external contributors follow the following steps:
+
+1. Fork a parent project.
+1. Create a merge request from the forked project that targets the `master` branch
+in the parent project.
+1. A pipeline runs on the merge request.
+1. A mainatiner from the parent project checks the pipeline result, and merge
+into a target branch if the latest pipeline has passed.
+
+Currently, those pipelines are created in a **forked** project, not in the
+parent project. This means you cannot completely trust the pipeline result,
+because, technically, external contributors can disguise their pipeline results
+by tweaking their GitLab Runner in the forked project.
+
+There are multiple reasons about why GitLab doesn't allow those pipelines to be
+created in the parent project, but one of the biggest reasons is security concern.
+External users could steal secret variables from the parent project by modifying
+.gitlab-ci.yml, which could be some sort of credentials. This should not happen.
+
+We're discussing a secure solution of running pipelines for merge requests
+that submitted from forked projects,
+see [the issue about the permission extension](https://gitlab.com/gitlab-org/gitlab-ce/issues/23902).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index bffb0121603..8ed04e04e53 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -148,7 +148,7 @@ file. The parameter is of the form:
variables[key]=value
```
-This information is also exposed in the UI.
+This information is also exposed in the UI. Please note that _values_ are only viewable by Owners and Maintainers.
![Job variables in UI](img/trigger_variables.png)
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 0c2a761cfa9..f862155b47f 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index fd81a67dca0..87799be8ab4 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -40,73 +40,84 @@ Starting with GitLab 9.0, we have deprecated some variables. Read the
strongly advised to use the new variables as we will remove the old ones in
future GitLab releases.**
-| Variable | GitLab | Runner | Description |
-|-------------------------------- |--------|--------|-------------|
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
-| **CI** | all | 0.4 | Mark that job is executed in CI environment |
-| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
-| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
-| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
-| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
-| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
-| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
-| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
-| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
-| **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_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| **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_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. |
-| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
-| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
-| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
-| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
-| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
-| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
-| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
-| **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
-| **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
-| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
-| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
-| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
-| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
-| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
-| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
-| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
-| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
-| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
-| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
-| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
-| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
-| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
-| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
-| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
-| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
-| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
-| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
-| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
-| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
-| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
-| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
-| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
-| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
-| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
-| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
-| **CI_SERVER_VERSION_MAJOR** | 11.4 | all | GitLab version major component |
-| **CI_SERVER_VERSION_MINOR** | 11.4 | all | GitLab version minor component |
-| **CI_SERVER_VERSION_PATCH** | 11.4 | all | GitLab version patch component |
-| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
-| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
-| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
-| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
-| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
-| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
-| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
-| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
+| Variable | GitLab | Runner | Description |
+|-------------------------------------------|--------|--------|-------------|
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
+| **CI** | all | 0.4 | Mark that job is executed in CI environment |
+| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
+| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
+| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
+| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
+| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
+| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
+| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
+| **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_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **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_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. |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
+| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
+| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
+| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
+| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
+| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
+| **CI_MERGE_REQUEST_ID** | 11.6 | all | The ID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_IID** | 11.6 | all | The IID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_REF_PATH** | 11.6 | all | The ref path of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`) |
+| **CI_MERGE_REQUEST_PROJECT_ID** | 11.6 | all | The ID of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_PROJECT_PATH** | 11.6 | all | The path of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`) |
+| **CI_MERGE_REQUEST_PROJECT_URL** | 11.6 | all | The URL of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`) |
+| **CI_MERGE_REQUEST_TARGET_BRANCH_NAME** | 11.6 | all | The target branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_ID** | 11.6 | all | The ID of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_PATH** | 11.6 | all | The path of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_URL** | 11.6 | all | The URL of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_BRANCH_NAME** | 11.6 | all | The source branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
+| **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
+| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
+| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
+| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
+| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
+| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
+| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
+| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
+| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
+| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
+| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
+| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
+| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
+| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
+| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
+| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
+| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
+| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
+| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
+| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
+| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
+| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
+| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
+| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
+| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
+| **CI_SERVER_VERSION_MAJOR** | 11.4 | all | GitLab version major component |
+| **CI_SERVER_VERSION_MINOR** | 11.4 | all | GitLab version minor component |
+| **CI_SERVER_VERSION_PATCH** | 11.4 | all | GitLab version patch component |
+| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
+| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
+| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
+| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
+| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
+| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
+| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
+| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
## GitLab 9.0 renaming
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index af7e41db443..1277d1fdf8b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -342,15 +342,16 @@ In addition, `only` and `except` allow the use of special keywords:
| **Value** | **Description** |
| --------- | ---------------- |
-| `branches` | When a branch is pushed. |
-| `tags` | When a tag is pushed. |
-| `api` | When pipeline has been triggered by a second pipelines API (not triggers API). |
-| `external` | When using CI services other than GitLab. |
-| `pipelines` | For multi-project triggers, created using the API with `CI_JOB_TOKEN`. |
-| `pushes` | Pipeline is triggered by a `git push` by the user. |
-| `schedules` | For [scheduled pipelines][schedules]. |
-| `triggers` | For pipelines created using a trigger token. |
-| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
+| `branches` | When a git reference of a pipeline is a branch. |
+| `tags` | When a git reference of a pipeline is a tag. |
+| `api` | When pipeline has been triggered by a second pipelines API (not triggers API). |
+| `external` | When using CI services other than GitLab. |
+| `pipelines` | For multi-project triggers, created using the API with `CI_JOB_TOKEN`. |
+| `pushes` | Pipeline is triggered by a `git push` by the user. |
+| `schedules` | For [scheduled pipelines][schedules]. |
+| `triggers` | For pipelines created using a trigger token. |
+| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
+| `merge_requests` | When a merge request is created or updated (See [pipelines for merge requests](../merge_request_pipelines/index.md)). |
In the example below, `job` will run only for refs that start with `issue-`,
whereas all branches will be skipped:
@@ -391,6 +392,24 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except master.
+If a job does not have neither `only` nor `except` rule,
+`only: ['branches', 'tags']` is set by default.
+
+For example,
+
+```yaml
+job:
+ script: echo 'test'
+```
+
+is translated to
+
+```yaml
+job:
+ script: echo 'test'
+ only: ['branches', 'tags']
+```
+
## `only` and `except` (complex)
> `refs` and `kubernetes` policies introduced in GitLab 10.0
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index b7990e1b558..55aed023325 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -368,6 +368,16 @@ You can combine one or more of the following:
= link_to 'Help page', help_page_path('user/permissions')
```
+### GitLab `/help` tests
+
+Several [rspec tests](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/features/help_pages_spec.rb)
+are run to ensure GitLab documentation renders and works correctly. In particular, that [main docs landing page](../../README.md) will work correctly from `/help`.
+For example, [GitLab.com's `/help`](https://gitlab.com/help).
+
+CAUTION: **Caution:**
+Because the rspec tests only run in a full pipeline, and not a special [docs-only pipeline](#branch-naming), it is possible
+to merge changes that will break `master` from a merge request with a successful docs-only pipeline run.
+
## General Documentation vs Technical Articles
### General documentation
@@ -552,6 +562,7 @@ Currently, the following tests are in place:
As CE is merged into EE once a day, it's important to avoid merge conflicts.
Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
essential to avoid them.
+1. In a full pipeline, tests for [`/help`](#gitlab-help-tests).
### Linting
diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md
index 1019a1fd0e2..b6161cd6163 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -113,7 +113,15 @@ feature flag. You can stub a feature flag as follows:
stub_feature_flags(my_feature_flag: false)
```
-## Enabling a feature flag
+## Enabling a feature flag (in development)
+
+In the rails console (`rails c`), enter the following command to enable your feature flag
+
+```ruby
+Feature.enable(:feature_flag_name)
+```
+
+## Enabling a feature flag (in production)
Check how to [roll out changes using feature flags](rolling_out_changes_using_feature_flags.md).
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 63e7497cbbc..7885cffd107 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -132,7 +132,8 @@ in three places:
- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops)
- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section
-- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+- or at the project as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+- or at the group level as a variable: `AUTO_DEVOPS_DOMAIN`
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:
@@ -203,6 +204,12 @@ and verifying that your app is deployed as a review app in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
+NOTE: **Note:**
+Auto DevOps is not supported for a group with multiple clusters, as it
+is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
+level. This will be resolved in the future with the [following issue](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+
## Enabling/Disabling Auto DevOps
When first using Auto Devops, review the [requirements](#requirements) to ensure all necessary components to make
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
new file mode 100644
index 00000000000..adc43921d47
--- /dev/null
+++ b/doc/user/group/clusters/index.md
@@ -0,0 +1,126 @@
+# Group-level Kubernetes clusters
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) in GitLab 11.6.
+
+CAUTION: **Warning:**
+Group Cluster integration is currently in **Beta**.
+
+## Overview
+
+Similar to [project Kubernetes
+clusters](../../project/clusters/index.md), Group-level Kubernetes
+clusters allow you to connect a Kubernetes cluster to your group,
+enabling you to use the same cluster across multiple projects.
+
+## Installing applications
+
+GitLab provides a one-click install for various applications that can be
+added directly to your cluster.
+
+NOTE: **Note:**
+Applications will be installed in a dedicated namespace called
+`gitlab-managed-apps`. If you have added an existing Kubernetes cluster
+with Tiller already installed, you should be careful as GitLab cannot
+detect it. In this event, installing Tiller via the applications will
+result in the cluster having it twice. This can lead to confusion during
+deployments.
+
+| Application | GitLab version | Description | Helm Chart |
+| ----------- | -------------- | ----------- | ---------- |
+| [Helm Tiller](https://docs.helm.sh) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
+| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
+
+## RBAC compatibility
+
+For each project under a group with a Kubernetes cluster, GitLab will
+create a restricted service account with [`edit`
+privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+in the project namespace.
+
+NOTE: **Note:**
+RBAC support was introduced in
+[GitLab 11.4](https://gitlab.com/gitlab-org/gitlab-ce/issues/29398), and
+Project namespace restriction was introduced in
+[GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716).
+
+## Cluster precedence
+
+GitLab will use the project's cluster before using any cluster belonging
+to the group containing the project if the project's cluster is available and not disabled.
+
+In the case of sub-groups, GitLab will use the cluster of the closest ancestor group
+to the project, provided the cluster is not disabled.
+
+## Multiple Kubernetes clusters **[PREMIUM]**
+
+With GitLab Premium, you can associate more than one Kubernetes clusters to your
+group. That way you can have different clusters for different environments,
+like dev, staging, production, etc.
+
+Add another cluster similar to the first one and make sure to
+[set an environment scope](#environment-scopes) that will
+differentiate the new cluster from the rest.
+
+NOTE: **Note:**
+Auto DevOps is not supported for a group with multiple clusters, as it
+is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
+level. This will be resolved in the future with the [following issue](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+
+## Environment scopes **[PREMIUM]**
+
+When adding more than one Kubernetes cluster to your project, you need
+to differentiate them with an environment scope. The environment scope
+associates clusters with [environments](../../../ci/environments.md)
+similar to how the [environment-specific
+variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
+work.
+
+While evaluating which environment matches the environment scope of a
+cluster, [cluster precedence](#cluster-precedence) will take
+effect. The cluster at the project level will take precedence, followed
+by the closest ancestor group, followed by that groups' parent and so
+on.
+
+For example, let's say we have the following Kubernetes clusters:
+
+| Cluster | Environment scope | Where |
+| ---------- | ------------------- | ----------|
+| Project | `*` | Project |
+| Staging | `staging/*` | Project |
+| Production | `production/*` | Project |
+| Test | `test` | Group |
+| Development| `*` | Group |
+
+
+And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md):
+
+```yaml
+stages:
+- test
+- deploy
+
+test:
+ stage: test
+ script: sh test
+
+deploy to staging:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: staging/$CI_COMMIT_REF_NAME
+ url: https://staging.example.com/
+
+deploy to production:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: production/$CI_COMMIT_REF_NAME
+ url: https://example.com/
+```
+
+The result will then be:
+
+- The Project cluster will be used for the `test` job.
+- The Staging cluster will be used for the `deploy to staging` job.
+- The Production cluster will be used for the `deploy to production` job.
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 36b9318c0e0..5fea683a7fd 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -269,6 +269,7 @@ Define project templates at a group-level by setting a group as a template sourc
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
+- **Kubernetes cluster integration**: connect your GitLab group with [Kubernetes clusters](clusters/index.md).
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
for the group. **[STARTER ONLY]**
-- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
+- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 66ad1843e93..e40525d2577 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -17,6 +17,11 @@ your account with Google Kubernetes Engine (GKE) so that you can [create new
clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab,
or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster).
+NOTE: **Note:**
+From [GitLab 11.6](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) you
+can also associate a Kubernetes cluster to your groups. Learn more about
+[group Kubernetes clusters](../../group/clusters/index.md).
+
## Adding and creating a new GKE cluster via GitLab
TIP: **Tip:**
@@ -245,22 +250,24 @@ install it manually.
## Installing applications
-GitLab provides a one-click install for various applications which will be
-added directly to your configured cluster. Those applications are needed for
-[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
+GitLab provides 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).
NOTE: **Note:**
With the exception of Knative, the applications will be installed in a dedicated namespace called
`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
with Tiller already installed, you should be careful as GitLab cannot
-detect it. By installing it via the applications will result into having it
-twice, which can lead to confusion during deployments.
+detect it. In this event, installing Tiller via the applications will
+result in the cluster having it twice. This can lead to confusion during
+deployments.
| Application | GitLab version | Description | Helm Chart |
| ----------- | :------------: | ----------- | --------------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
-| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up to date. The email address used by Let's Encrypt registration will be taken from the GitLab user that installed Cert Manager on the cluster. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
+| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
@@ -347,17 +354,13 @@ to reach your apps. This heavily depends on your domain provider, but in case
you aren't sure, just create an A record with a wildcard host like
`*.example.com.`.
-## Setting the environment scope
+## Setting the environment scope **[PREMIUM]**
-NOTE: **Note:**
-This is only available for [GitLab Premium][ee] where you can add more than
-one Kubernetes cluster.
-
-When adding more than one Kubernetes clusters to your project, you need to
-differentiate them with an environment scope. The environment scope associates
-clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
-similar to how the
-[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
+When adding more than one Kubernetes clusters to your project, you need
+to differentiate them with an environment scope. The environment scope
+associates clusters with [environments](../../../ci/environments.md)
+similar to how the [environment-specific
+variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
work.
The default environment scope is `*`, which means all jobs, regardless of their
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 5b54b6ecdd5..85d8d804133 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -259,6 +259,16 @@ all your changes will be available to preview by anyone with the Review Apps lin
[Read more about Review Apps.](../../../ci/review_apps/index.md)
+## Pipelines for merge requests
+
+When a developer updates a merge request, a pipeline should quickly report back
+its result to the developer, but often pipelines take long time to complete
+because general branch pipelines contain unnecessary jobs from the merge request standpoint.
+You can customize a specific pipeline structure for merge requests in order to
+speed the cycle up by running only important jobs.
+
+Learn more about [pipelines for merge requests](../../../ci/merge_request_pipelines/index.md).
+
## Pipeline status in merge requests
If you've set up [GitLab CI/CD](../../../ci/README.md) in your project,
diff --git a/lib/api/api.rb b/lib/api/api.rb
index a4bf0d77eb1..8abb24e6f69 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -20,7 +20,8 @@ module API
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
- Gitlab::GrapeLogging::Loggers::PerfLogger.new
+ Gitlab::GrapeLogging::Loggers::PerfLogger.new,
+ Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
]
allow_access_with_scope :api
@@ -84,7 +85,6 @@ module API
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
- helpers ::SentryHelper
helpers ::API::Helpers
helpers ::API::Helpers::CommonHelpers
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 9fda73d5b92..2cceb2ec798 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -368,10 +368,10 @@ module API
end
def handle_api_exception(exception)
- if sentry_enabled? && report_exception?(exception)
+ if report_exception?(exception)
define_params_for_grape_middleware
- sentry_context
- Raven.capture_exception(exception, extra: params)
+ Gitlab::Sentry.context(current_user)
+ Gitlab::Sentry.track_acceptable_exception(exception, extra: params)
end
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 06a57e3cd6f..3cc09f6ac3f 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -6,20 +6,35 @@ module API
before { authenticate! }
+ helpers do
+ params :optional_list_params_ee do
+ # EE::API::Namespaces would override this helper
+ end
+
+ # EE::API::Namespaces would override this method
+ def custom_namespace_present_options
+ {}
+ end
+ end
+
resource :namespaces do
desc 'Get a namespaces list' do
success Entities::Namespace
end
params do
optional :search, type: String, desc: "Search query for namespaces"
+
use :pagination
+ use :optional_list_params_ee
end
get do
namespaces = current_user.admin ? Namespace.all : current_user.namespaces
namespaces = namespaces.search(params[:search]) if params[:search].present?
- present paginate(namespaces), with: Entities::Namespace, current_user: current_user
+ options = { with: Entities::Namespace, current_user: current_user }
+
+ present paginate(namespaces), options.reverse_merge(custom_namespace_present_options)
end
desc 'Get a namespace by ID' do
diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb
new file mode 100644
index 00000000000..a27d18facd1
--- /dev/null
+++ b/lib/banzai/filter/front_matter_filter.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ class FrontMatterFilter < HTML::Pipeline::Filter
+ DELIM_LANG = {
+ '---' => 'yaml',
+ '+++' => 'toml',
+ ';;;' => 'json'
+ }.freeze
+
+ DELIM = Regexp.union(DELIM_LANG.keys)
+
+ PATTERN = %r{
+ \A(?:[^\r\n]*coding:[^\r\n]*)? # optional encoding line
+ \s*
+ ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*) # opening front matter marker (optional language specifier)
+ \s*
+ ^(?<front_matter>.*?) # front matter (not greedy)
+ \s*
+ ^\k<delim> # closing front matter marker
+ \s*
+ }mx
+
+ def call
+ html.sub(PATTERN) do |_match|
+ lang = $~[:lang].presence || DELIM_LANG[$~[:delim]]
+
+ ["```#{lang}", $~[:front_matter], "```", "\n"].join("\n")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 328c8c1803b..c70c3f0c04e 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
module Filter
# HTML filter that replaces milestone references with links.
class MilestoneReferenceFilter < AbstractReferenceFilter
+ include Gitlab::Utils::StrongMemoize
+
self.reference_type = :milestone
def self.object_class
@@ -13,16 +15,34 @@ module Banzai
# Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate
# between group and project milestones.
- def find_object(project, id)
- return unless project.is_a?(Project)
+ def find_object(parent, id)
+ return unless valid_context?(parent)
- find_milestone_with_finder(project, id: id)
+ find_milestone_with_finder(parent, id: id)
end
- def find_object_from_link(project, iid)
- return unless project.is_a?(Project)
+ def find_object_from_link(parent, iid)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, iid: iid)
+ end
+
+ def valid_context?(parent)
+ strong_memoize(:valid_context) do
+ group_context?(parent) || project_context?(parent)
+ end
+ end
+
+ def group_context?(parent)
+ strong_memoize(:group_context) do
+ parent.is_a?(Group)
+ end
+ end
- find_milestone_with_finder(project, iid: iid)
+ def project_context?(parent)
+ strong_memoize(:project_context) do
+ parent.is_a?(Project)
+ end
end
def references_in(text, pattern = Milestone.reference_pattern)
@@ -44,13 +64,15 @@ module Banzai
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
project_path = full_project_path(namespace_ref, project_ref)
- project = parent_from_ref(project_path)
- return unless project && project.is_a?(Project)
+ # Returns group if project is not found by path
+ parent = parent_from_ref(project_path)
+
+ return unless parent
milestone_params = milestone_params(milestone_id, milestone_name)
- find_milestone_with_finder(project, milestone_params)
+ find_milestone_with_finder(parent, milestone_params)
end
def milestone_params(iid, name)
@@ -61,16 +83,28 @@ module Banzai
end
end
- def find_milestone_with_finder(project, params)
- finder_params = { project_ids: [project.id], order: nil, state: 'all' }
+ def find_milestone_with_finder(parent, params)
+ finder_params = milestone_finder_params(parent, params[:iid].present?)
+
+ MilestonesFinder.new(finder_params).find_by(params)
+ end
- # We don't support IID lookups for group milestones, because IIDs can
- # clash between group and project milestones.
- if project.group && !params[:iid]
- finder_params[:group_ids] = project.group.self_and_ancestors_ids
+ def milestone_finder_params(parent, find_by_iid)
+ { order: nil, state: 'all' }.tap do |params|
+ params[:project_ids] = parent.id if project_context?(parent)
+
+ # We don't support IID lookups because IIDs can clash between
+ # group/project milestones and group/subgroup milestones.
+ params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
end
+ end
- MilestonesFinder.new(finder_params).find_by(params)
+ def self_and_ancestors_ids(parent)
+ if group_context?(parent)
+ parent.self_and_ancestors_ids
+ elsif project_context?(parent)
+ parent.group&.self_and_ancestors_ids
+ end
end
def url_for_object(milestone, project)
diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb
deleted file mode 100644
index 295964dd75d..00000000000
--- a/lib/banzai/filter/yaml_front_matter_filter.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class YamlFrontMatterFilter < HTML::Pipeline::Filter
- DELIM = '---'.freeze
-
- # Hat-tip to Middleman: https://git.io/v2e0z
- PATTERN = %r{
- \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
- (?<start>#{DELIM})[ ]*\r?\n
- (?<frontmatter>.*?)[ ]*\r?\n?
- ^(?<stop>#{DELIM})[ ]*\r?\n?
- \r?\n?
- (?<content>.*)
- }mx.freeze
-
- def call
- match = PATTERN.match(html)
-
- return html unless match
-
- "```yaml\n#{match['frontmatter']}\n```\n\n#{match['content']}"
- end
- end
- end
-end
diff --git a/lib/banzai/pipeline/pre_process_pipeline.rb b/lib/banzai/pipeline/pre_process_pipeline.rb
index c937f783180..4c2b4ca1665 100644
--- a/lib/banzai/pipeline/pre_process_pipeline.rb
+++ b/lib/banzai/pipeline/pre_process_pipeline.rb
@@ -5,7 +5,7 @@ module Banzai
class PreProcessPipeline < BasePipeline
def self.filters
FilterArray[
- Filter::YamlFrontMatterFilter,
+ Filter::FrontMatterFilter,
Filter::BlockquoteFenceFilter,
]
end
diff --git a/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb b/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb
new file mode 100644
index 00000000000..2f76f2f7434
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Class that will create fill the project_repositories table
+ # for all projects that are on hashed storage and an entry is
+ # is missing in this table.
+ class BackfillHashedProjectRepositories
+ # Shard model
+ class Shard < ActiveRecord::Base
+ self.table_name = 'shards'
+ end
+
+ # Class that will find or create the shard by name.
+ # There is only a small set of shards, which would
+ # not change quickly, so look them up from memory
+ # instead of hitting the DB each time.
+ class ShardFinder
+ def find_shard_id(name)
+ shard_id = shards.fetch(name, nil)
+ return shard_id if shard_id.present?
+
+ Shard.transaction(requires_new: true) do
+ create!(name)
+ end
+ rescue ActiveRecord::RecordNotUnique
+ reload!
+ retry
+ end
+
+ private
+
+ def create!(name)
+ Shard.create!(name: name).tap { |shard| @shards[name] = shard.id }
+ end
+
+ def shards
+ @shards ||= reload!
+ end
+
+ def reload!
+ @shards = Hash[*Shard.all.map { |shard| [shard.name, shard.id] }.flatten]
+ end
+ end
+
+ # ProjectRegistry model
+ class ProjectRepository < ActiveRecord::Base
+ self.table_name = 'project_repositories'
+
+ belongs_to :project, inverse_of: :project_repository
+ end
+
+ # Project model
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ HASHED_PATH_PREFIX = '@hashed'
+
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
+
+ has_one :project_repository, inverse_of: :project
+
+ class << self
+ def on_hashed_storage
+ where(Project.arel_table[:storage_version]
+ .gteq(HASHED_STORAGE_FEATURES[:repository]))
+ end
+
+ def without_project_repository
+ joins(left_outer_join_project_repository)
+ .where(ProjectRepository.arel_table[:project_id].eq(nil))
+ end
+
+ def left_outer_join_project_repository
+ projects_table = Project.arel_table
+ repository_table = ProjectRepository.arel_table
+
+ projects_table
+ .join(repository_table, Arel::Nodes::OuterJoin)
+ .on(projects_table[:id].eq(repository_table[:project_id]))
+ .join_sources
+ end
+ end
+
+ def hashed_storage?
+ self.storage_version && self.storage_version >= 1
+ end
+
+ def hashed_disk_path
+ "#{HASHED_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}/#{disk_hash}"
+ end
+
+ def disk_hash
+ @disk_hash ||= Digest::SHA2.hexdigest(id.to_s)
+ end
+ end
+
+ def perform(start_id, stop_id)
+ Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id))
+ end
+
+ private
+
+ def project_repositories(start_id, stop_id)
+ Project.on_hashed_storage
+ .without_project_repository
+ .where(id: start_id..stop_id)
+ .map { |project| build_attributes_for_project(project) }
+ .compact
+ end
+
+ def build_attributes_for_project(project)
+ return unless project.hashed_storage?
+
+ {
+ project_id: project.id,
+ shard_id: find_shard_id(project.repository_storage),
+ disk_path: project.hashed_disk_path
+ }
+ end
+
+ def find_shard_id(repository_storage)
+ shard_finder.find_shard_id(repository_storage)
+ end
+
+ def shard_finder
+ @shard_finder ||= ShardFinder.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/branch_push_merge_commit_analyzer.rb b/lib/gitlab/branch_push_merge_commit_analyzer.rb
new file mode 100644
index 00000000000..a8f601f2451
--- /dev/null
+++ b/lib/gitlab/branch_push_merge_commit_analyzer.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+module Gitlab
+ # Analyse a graph of commits from a push to a branch,
+ # for each commit, analyze that if it is the head of a merge request,
+ # then what should its merge_commit be, relative to the branch.
+ #
+ # A----->B----->C----->D target branch
+ # | ^
+ # | |
+ # +-->E----->F--+ merged branch
+ # | ^
+ # | |
+ # +->G--+
+ #
+ # (See merge-commit-analyze-after branch in gitlab-test)
+ #
+ # Assuming
+ # - A is already in remote
+ # - B~D are all in its own branch with its own merge request, targeting the target branch
+ #
+ # When D is finally pushed to the target branch,
+ # what are the merge commits for all the other merge requests?
+ #
+ # We can walk backwards from the HEAD commit D,
+ # and find status of its parents.
+ # First we determine if commit belongs to the target branch (i.e. A, B, C, D),
+ # and then determine its merge commit.
+ #
+ # +--------+-----------------+--------------+
+ # | Commit | Direct ancestor | Merge commit |
+ # +--------+-----------------+--------------+
+ # | D | Y | D |
+ # +--------+-----------------+--------------+
+ # | C | Y | C |
+ # +--------+-----------------+--------------+
+ # | F | | C |
+ # +--------+-----------------+--------------+
+ # | B | Y | B |
+ # +--------+-----------------+--------------+
+ # | E | | C |
+ # +--------+-----------------+--------------+
+ # | G | | C |
+ # +--------+-----------------+--------------+
+ #
+ # By examining the result, it can be said that
+ #
+ # - If commit is direct ancestor of HEAD, its merge commit is itself.
+ # - Otherwise, the merge commit is the same as its child's merge commit.
+ #
+ class BranchPushMergeCommitAnalyzer
+ class CommitDecorator < SimpleDelegator
+ attr_accessor :merge_commit
+ attr_writer :direct_ancestor # boolean
+
+ def direct_ancestor?
+ @direct_ancestor
+ end
+
+ # @param child_commit [CommitDecorator]
+ # @param first_parent [Boolean] whether `self` is the first parent of `child_commit`
+ def set_merge_commit(child_commit:)
+ @merge_commit ||= direct_ancestor? ? self : child_commit.merge_commit
+ end
+ end
+
+ # @param commits [Array] list of commits, must be ordered from the child (tip) of the graph back to the ancestors
+ def initialize(commits, relevant_commit_ids: nil)
+ @commits = commits
+ @id_to_commit = {}
+ @commits.each do |commit|
+ @id_to_commit[commit.id] = CommitDecorator.new(commit)
+
+ if relevant_commit_ids
+ relevant_commit_ids.delete(commit.id)
+ break if relevant_commit_ids.empty? # Only limit the analyze up to relevant_commit_ids
+ end
+ end
+
+ analyze
+ end
+
+ def get_merge_commit(id)
+ get_commit(id).merge_commit.id
+ end
+
+ private
+
+ def analyze
+ head_commit = get_commit(@commits.first.id)
+ head_commit.direct_ancestor = true
+ head_commit.merge_commit = head_commit
+
+ mark_all_direct_ancestors(head_commit)
+
+ # Analyzing a commit requires its child commit be analyzed first,
+ # which is the case here since commits are ordered from child to parent.
+ @id_to_commit.each_value do |commit|
+ analyze_parents(commit)
+ end
+ end
+
+ def analyze_parents(commit)
+ commit.parent_ids.each do |parent_commit_id|
+ parent_commit = get_commit(parent_commit_id)
+
+ next unless parent_commit # parent commit may not be part of new commits
+
+ parent_commit.set_merge_commit(child_commit: commit)
+ end
+ end
+
+ # Mark all direct ancestors.
+ # If child commit is a direct ancestor, its first parent is also a direct ancestor.
+ # We assume direct ancestors matches the trail of the target branch over time,
+ # This assumption is correct most of the time, especially for gitlab managed merges,
+ # but there are exception cases which can't be solved (https://stackoverflow.com/a/49754723/474597)
+ def mark_all_direct_ancestors(commit)
+ loop do
+ commit = get_commit(commit.parent_ids.first)
+
+ break unless commit
+
+ commit.direct_ancestor = true
+ end
+ end
+
+ def get_commit(id)
+ @id_to_commit[id]
+ end
+ end
+end
diff --git a/lib/gitlab/correlation_id.rb b/lib/gitlab/correlation_id.rb
new file mode 100644
index 00000000000..0f9bde4390e
--- /dev/null
+++ b/lib/gitlab/correlation_id.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CorrelationId
+ LOG_KEY = 'correlation_id'.freeze
+
+ class << self
+ def use_id(correlation_id, &blk)
+ # always generate a id if null is passed
+ correlation_id ||= new_id
+
+ ids.push(correlation_id || new_id)
+
+ begin
+ yield(current_id)
+ ensure
+ ids.pop
+ end
+ end
+
+ def current_id
+ ids.last
+ end
+
+ def current_or_new_id
+ current_id || new_id
+ end
+
+ private
+
+ def ids
+ Thread.current[:correlation_id] ||= []
+ end
+
+ def new_id
+ SecureRandom.uuid
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 134d1e7a724..d9578852db6 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -975,9 +975,10 @@ into similar problems in the future (e.g. when new tables are created).
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
jobs = []
+ table_name = model_class.quoted_table_name
model_class.each_batch(of: batch_size) do |relation|
- start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+ start_id, end_id = relation.pluck("MIN(#{table_name}.id), MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
new file mode 100644
index 00000000000..558699a6318
--- /dev/null
+++ b/lib/gitlab/git/object_pool.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ class ObjectPool
+ # GL_REPOSITORY has to be passed for Gitlab::Git::Repositories, but not
+ # used for ObjectPools.
+ GL_REPOSITORY = ""
+
+ delegate :exists?, :size, to: :repository
+ delegate :delete, to: :object_pool_service
+
+ attr_reader :storage, :relative_path, :source_repository
+
+ def initialize(storage, relative_path, source_repository)
+ @storage = storage
+ @relative_path = relative_path
+ @source_repository = source_repository
+ end
+
+ def create
+ object_pool_service.create(source_repository)
+ end
+
+ def link(to_link_repo)
+ remote_name = to_link_repo.object_pool_remote_name
+ repository.set_config(
+ "remote.#{remote_name}.url" => relative_path_to(to_link_repo.relative_path),
+ "remote.#{remote_name}.tagOpt" => "--no-tags",
+ "remote.#{remote_name}.fetch" => "+refs/*:refs/remotes/#{remote_name}/*"
+ )
+
+ object_pool_service.link_repository(to_link_repo)
+ end
+
+ def gitaly_object_pool
+ Gitaly::ObjectPool.new(repository: to_gitaly_repository)
+ end
+
+ def to_gitaly_repository
+ Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY)
+ end
+
+ # Allows for reusing other RPCs by 'tricking' Gitaly to think its a repository
+ def repository
+ @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY)
+ end
+
+ private
+
+ def object_pool_service
+ @object_pool_service ||= Gitlab::GitalyClient::ObjectPoolService.new(self)
+ end
+
+ def relative_path_to(pool_member_path)
+ pool_path = Pathname.new("#{relative_path}#{File::SEPARATOR}")
+
+ Pathname.new(pool_member_path).relative_path_from(pool_path).to_s
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 0a541031884..5bbedc9d5e3 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -69,6 +69,13 @@ module Gitlab
attr_reader :storage, :gl_repository, :relative_path
+ # This remote name has to be stable for all types of repositories that
+ # can join an object pool. If it's structure ever changes, a migration
+ # has to be performed on the object pools to update the remote names.
+ # Else the pool can't be updated anymore and is left in an inconsistent
+ # state.
+ alias_method :object_pool_remote_name, :gl_repository
+
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
def initialize(storage, relative_path, gl_repository)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 9be553a8b86..255601382b1 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -193,6 +193,7 @@ module Gitlab
feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
+ metadata['correlation_id'] = Gitlab::CorrelationId.current_id if Gitlab::CorrelationId.current_id
metadata.merge!(server_feature_flags)
diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb
new file mode 100644
index 00000000000..272ce73ad64
--- /dev/null
+++ b/lib/gitlab/gitaly_client/object_pool_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GitalyClient
+ class ObjectPoolService
+ attr_reader :object_pool, :storage
+
+ def initialize(object_pool)
+ @object_pool = object_pool.gitaly_object_pool
+ @storage = object_pool.storage
+ end
+
+ def create(repository)
+ request = Gitaly::CreateObjectPoolRequest.new(
+ object_pool: object_pool,
+ origin: repository.gitaly_repository)
+
+ GitalyClient.call(storage, :object_pool_service, :create_object_pool, request)
+ end
+
+ def delete
+ request = Gitaly::DeleteObjectPoolRequest.new(object_pool: object_pool)
+
+ GitalyClient.call(storage, :object_pool_service, :delete_object_pool, request)
+ end
+
+ def link_repository(repository)
+ request = Gitaly::LinkRepositoryToObjectPoolRequest.new(
+ object_pool: object_pool,
+ repository: repository.gitaly_repository
+ )
+
+ GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool,
+ request, timeout: GitalyClient.fast_timeout)
+ end
+
+ def unlink_repository(repository)
+ request = Gitaly::UnlinkRepositoryFromObjectPoolRequest.new(repository: repository.gitaly_repository)
+
+ GitalyClient.call(storage, :object_pool_service, :unlink_repository_from_object_pool,
+ request, timeout: GitalyClient.fast_timeout)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
new file mode 100644
index 00000000000..fa4c5d86d44
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# This module adds additional correlation id the grape logger
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class CorrelationIdLogger < ::GrapeLogging::Loggers::Base
+ def parameters(_, _)
+ { Gitlab::CorrelationId::LOG_KEY => Gitlab::CorrelationId.current_id }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index fde8561c16c..d10d4f2f746 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -143,6 +143,7 @@ excluded_attributes:
statuses:
- :trace
- :token
+ - :token_encrypted
- :when
- :artifacts_file
- :artifacts_metadata
diff --git a/lib/gitlab/json_logger.rb b/lib/gitlab/json_logger.rb
index 3bff77731f6..a5a5759cc89 100644
--- a/lib/gitlab/json_logger.rb
+++ b/lib/gitlab/json_logger.rb
@@ -10,6 +10,7 @@ module Gitlab
data = {}
data[:severity] = severity
data[:time] = timestamp.utc.iso8601(3)
+ data[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
case message
when String
diff --git a/lib/gitlab/middleware/correlation_id.rb b/lib/gitlab/middleware/correlation_id.rb
new file mode 100644
index 00000000000..73542dd422e
--- /dev/null
+++ b/lib/gitlab/middleware/correlation_id.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# A dumb middleware that steals correlation id
+# and sets it as a global context for the request
+module Gitlab
+ module Middleware
+ class CorrelationId
+ include ActionView::Helpers::TagHelper
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ ::Gitlab::CorrelationId.use_id(correlation_id(env)) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def correlation_id(env)
+ if Gitlab.rails5?
+ request(env).request_id
+ else
+ request(env).uuid
+ end
+ end
+
+ def request(env)
+ ActionDispatch::Request.new(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 8079c5882c4..46d01964eac 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -3,7 +3,8 @@
module Gitlab
module Sentry
def self.enabled?
- Rails.env.production? && Gitlab::CurrentSettings.sentry_enabled?
+ (Rails.env.production? || Rails.env.development?) &&
+ Gitlab::CurrentSettings.sentry_enabled?
end
def self.context(current_user = nil)
@@ -31,7 +32,7 @@ module Gitlab
def self.track_exception(exception, issue_url: nil, extra: {})
track_acceptable_exception(exception, issue_url: issue_url, extra: extra)
- raise exception if should_raise?
+ raise exception if should_raise_for_dev?
end
# This should be used when you do not want to raise an exception in
@@ -43,7 +44,11 @@ module Gitlab
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
- Raven.capture_exception(exception, extra: extra)
+ tags = {
+ Gitlab::CorrelationId::LOG_KEY.to_sym => Gitlab::CorrelationId.current_id
+ }
+
+ Raven.capture_exception(exception, tags: tags, extra: extra)
end
end
@@ -55,7 +60,7 @@ module Gitlab
end
end
- def self.should_raise?
+ def self.should_raise_for_dev?
Rails.env.development? || Rails.env.test?
end
end
diff --git a/lib/gitlab/sidekiq_middleware/correlation_injector.rb b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
new file mode 100644
index 00000000000..b807b3a03ed
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class CorrelationInjector
+ def call(worker_class, job, queue, redis_pool)
+ job[Gitlab::CorrelationId::LOG_KEY] ||=
+ Gitlab::CorrelationId.current_or_new_id
+
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/correlation_logger.rb b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
new file mode 100644
index 00000000000..cb8ff4a6284
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class CorrelationLogger
+ def call(worker, job, queue)
+ correlation_id = job[Gitlab::CorrelationId::LOG_KEY]
+
+ Gitlab::CorrelationId.use_id(correlation_id) do
+ yield
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fc923bf1554..2aeb015ed09 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -141,6 +141,24 @@ msgstr ""
msgid "%{percent}%% complete"
msgstr ""
+msgid "%{strong_start}%{branch_count}%{strong_end} Branch"
+msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{strong_start}%{commit_count}%{strong_end} Commit"
+msgid_plural "%{strong_start}%{commit_count}%{strong_end} Commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{strong_start}%{human_size}%{strong_end} Files"
+msgstr ""
+
+msgid "%{strong_start}%{tag_count}%{strong_end} Tag"
+msgid_plural "%{strong_start}%{tag_count}%{strong_end} Tags"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] ""
@@ -333,16 +351,16 @@ msgstr ""
msgid "Activity"
msgstr ""
-msgid "Add Changelog"
+msgid "Add CHANGELOG"
msgstr ""
-msgid "Add Contribution guide"
+msgid "Add CONTRIBUTING"
msgstr ""
msgid "Add Kubernetes cluster"
msgstr ""
-msgid "Add Readme"
+msgid "Add README"
msgstr ""
msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message."
@@ -945,11 +963,6 @@ msgstr ""
msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
-msgid "Branch (%{branch_count})"
-msgid_plural "Branches (%{branch_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
@@ -1103,6 +1116,9 @@ msgstr ""
msgid "ByAuthor|by"
msgstr ""
+msgid "CHANGELOG"
+msgstr ""
+
msgid "CI / CD"
msgstr ""
@@ -1160,6 +1176,9 @@ msgstr ""
msgid "CICD|instance enabled"
msgstr ""
+msgid "CONTRIBUTING"
+msgstr ""
+
msgid "Callback URL"
msgstr ""
@@ -1202,9 +1221,6 @@ msgstr ""
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr ""
-msgid "Changelog"
-msgstr ""
-
msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
msgstr ""
@@ -1388,9 +1404,18 @@ msgstr ""
msgid "Clients"
msgstr ""
+msgid "Clone"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
+msgid "Clone with %{http_label}"
+msgstr ""
+
+msgid "Clone with SSH"
+msgstr ""
+
msgid "Close"
msgstr ""
@@ -1448,6 +1473,9 @@ msgstr ""
msgid "ClusterIntegration|Cert-Manager"
msgstr ""
+msgid "ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates are valid and up-to-date."
+msgstr ""
+
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
@@ -1562,6 +1590,12 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
+msgid "ClusterIntegration|Issuer Email"
+msgstr ""
+
+msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer. "
+msgstr ""
+
msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
@@ -1781,9 +1815,6 @@ msgstr ""
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|cert-manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing cert-manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates are valid and up to date."
-msgstr ""
-
msgid "ClusterIntegration|check the pricing here"
msgstr ""
@@ -1811,6 +1842,9 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Command line instructions"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -1831,11 +1865,6 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "Commit (%{commit_count})"
-msgid_plural "Commits (%{commit_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -2019,9 +2048,6 @@ msgstr ""
msgid "Contribution Charts"
msgstr ""
-msgid "Contribution guide"
-msgstr ""
-
msgid "Contributions for <strong>%{calendar_date}</strong>"
msgstr ""
@@ -2046,10 +2072,10 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
-msgid "Copy %{protocol} clone URL"
+msgid "Copy %{http_label} clone URL"
msgstr ""
-msgid "Copy HTTPS clone URL"
+msgid "Copy %{protocol} clone URL"
msgstr ""
msgid "Copy ID to clipboard"
@@ -2112,6 +2138,9 @@ msgstr ""
msgid "Create a new issue"
msgstr ""
+msgid "Create a new repository"
+msgstr ""
+
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
@@ -2870,6 +2899,12 @@ msgstr ""
msgid "Everyone can contribute"
msgstr ""
+msgid "Existing Git repository"
+msgstr ""
+
+msgid "Existing folder"
+msgstr ""
+
msgid "Expand"
msgstr ""
@@ -2978,9 +3013,6 @@ msgstr ""
msgid "Files"
msgstr ""
-msgid "Files (%{human_size})"
-msgstr ""
-
msgid "Filter"
msgstr ""
@@ -3107,6 +3139,9 @@ msgstr ""
msgid "Git"
msgstr ""
+msgid "Git global setup"
+msgstr ""
+
msgid "Git repository URL"
msgstr ""
@@ -3376,6 +3411,9 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide values"
+msgstr ""
+
msgid "Hide whitespace changes"
msgstr ""
@@ -4438,6 +4476,12 @@ msgstr ""
msgid "Notification events"
msgstr ""
+msgid "Notification setting"
+msgstr ""
+
+msgid "Notification setting - %{notification_title}"
+msgstr ""
+
msgid "NotificationEvent|Close issue"
msgstr ""
@@ -5349,6 +5393,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "README"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -5358,9 +5405,6 @@ msgstr ""
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr ""
-msgid "Readme"
-msgstr ""
-
msgid "Real-time features"
msgstr ""
@@ -5576,14 +5620,14 @@ msgstr ""
msgid "Retry verification"
msgstr ""
-msgid "Reveal Variables"
-msgstr ""
-
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
msgstr[1] ""
+msgid "Reveal values"
+msgstr ""
+
msgid "Revert this commit"
msgstr ""
@@ -6294,11 +6338,6 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
-msgid "Tag (%{tag_count})"
-msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Tags"
msgstr ""
@@ -7218,6 +7257,9 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
+msgid "Variables:"
+msgstr ""
+
msgid "Various container registry settings."
msgstr ""
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index ac92b2ca657..c2bd7fd9808 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -460,6 +460,14 @@ describe ApplicationController do
expect(controller.last_payload.has_key?(:response)).to be_falsey
end
+ it 'does log correlation id' do
+ Gitlab::CorrelationId.use_id('new-id') do
+ get :index
+ end
+
+ expect(controller.last_payload).to include('correlation_id' => 'new-id')
+ end
+
context '422 errors' do
it 'logs a response with a string' do
response = spy(ActionDispatch::Response, status: 422, body: 'Hello world', content_type: 'application/json', cookies: {})
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index f6c85102830..4b0dc4c9b69 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -226,9 +226,10 @@ describe GroupsController do
end
context 'searching' do
- # Remove as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/52271
before do
+ # Remove in https://gitlab.com/gitlab-org/gitlab-ce/issues/54643
stub_feature_flags(use_cte_for_group_issues_search: false)
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
end
it 'works with popularity sort' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 51a7cc63cef..fca313dafb1 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -401,18 +401,56 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
+ end
- get_show(id: job.id, format: :json)
+ context 'user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'returns a job_detail' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ end
+
+ it 'exposes trigger information and variables' do
+ expect(json_response['trigger']['short_token']).to eq 'toke'
+ expect(json_response['trigger']['variables'].length).to eq 1
+ end
+
+ it 'exposes correct variable properties' do
+ first_variable = json_response['trigger']['variables'].first
+
+ expect(first_variable['key']).to eq "TRIGGER_KEY_1"
+ expect(first_variable['value']).to eq "TRIGGER_VALUE_1"
+ expect(first_variable['public']).to eq false
+ end
end
- it 'exposes trigger information and variables' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('job/job_details')
- expect(json_response['trigger']['short_token']).to eq 'toke'
- expect(json_response['trigger']['variables'].length).to eq 1
- expect(json_response['trigger']['variables'].first['key']).to eq "TRIGGER_KEY_1"
- expect(json_response['trigger']['variables'].first['value']).to eq "TRIGGER_VALUE_1"
- expect(json_response['trigger']['variables'].first['public']).to eq false
+ context 'user is not a mantainer' do
+ before do
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'returns a job_detail' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ end
+
+ it 'exposes trigger information and variables' do
+ expect(json_response['trigger']['short_token']).to eq 'toke'
+ expect(json_response['trigger']['variables'].length).to eq 1
+ end
+
+ it 'exposes correct variable properties' do
+ first_variable = json_response['trigger']['variables'].first
+
+ expect(first_variable['key']).to eq "TRIGGER_KEY_1"
+ expect(first_variable['value']).to be_nil
+ expect(first_variable['public']).to eq false
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index e62523c65c9..7f15da859e5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -290,6 +290,20 @@ describe Projects::MergeRequestsController do
it_behaves_like 'update invalid issuable', MergeRequest
end
+
+ context 'two merge requests with the same source branch' do
+ it 'does not allow a closed merge request to be reopened if another one is open' do
+ merge_request.close!
+ create(:merge_request, source_project: merge_request.source_project, source_branch: merge_request.source_branch)
+
+ update_merge_request(state_event: 'reopen')
+
+ errors = assigns[:merge_request].errors
+
+ expect(errors[:validate_branches]).to include(/Another open merge request already exists for this source branch/)
+ expect(merge_request.reload).to be_closed
+ end
+ end
end
describe 'POST merge' do
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 69ec971bb75..70f79a47e63 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -19,12 +19,14 @@ describe Projects::Settings::RepositoryController do
end
describe 'PUT cleanup' do
+ before do
+ allow(RepositoryCleanupWorker).to receive(:perform_async)
+ end
+
def do_put!
object_map = fixture_file_upload('spec/fixtures/bfg_object_map.txt')
- Sidekiq::Testing.fake! do
- put :cleanup, namespace_id: project.namespace, project_id: project, project: { object_map: object_map }
- end
+ put :cleanup, namespace_id: project.namespace, project_id: project, project: { object_map: object_map }
end
context 'feature enabled' do
@@ -34,7 +36,7 @@ describe Projects::Settings::RepositoryController do
do_put!
expect(response).to redirect_to project_settings_repository_path(project)
- expect(RepositoryCleanupWorker.jobs.count).to eq(1)
+ expect(RepositoryCleanupWorker).to have_received(:perform_async).once
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 7849bec4762..576191a5788 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -279,7 +279,7 @@ describe ProjectsController do
expected_query = /#{public_project.fork_network.find_forks_in(other_user.namespace).to_sql}/
expect { get(:show, namespace_id: public_project.namespace, id: public_project) }
- .not_to exceed_query_limit(1).for_query(expected_query)
+ .not_to exceed_query_limit(2).for_query(expected_query)
end
end
end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 2ed0844ed47..265a4643f46 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -1,5 +1,26 @@
FactoryBot.define do
factory :pool_repository do
- shard
+ shard { Shard.by_name("default") }
+ state :none
+
+ before(:create) do |pool|
+ pool.source_project = create(:project, :repository)
+ end
+
+ trait :scheduled do
+ state :scheduled
+ end
+
+ trait :failed do
+ state :failed
+ end
+
+ trait :ready do
+ state :ready
+
+ after(:create) do |pool|
+ pool.create_object_pool
+ end
+ end
end
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 71d715237f5..8918a7b7b9c 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -70,6 +70,44 @@ describe 'Clusters Applications', :js do
end
end
+ context 'when user installs Cert Manager' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ click_button 'Install'
+ end
+ end
+
+ it 'shows status transition' do
+ def email_form_value
+ page.find('.js-email').value
+ end
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ expect(email_form_value).to eq(cluster.user.email)
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
+
+ page.find('.js-email').set("new_email@example.org")
+ Clusters::Cluster.last.application_cert_manager.make_installing!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_cert_manager.make_installed!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
+ end
+
+ expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
+ end
+ end
+
context 'when user installs Ingress' do
context 'when user installs application: Ingress' do
before do
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index d7c4abffddd..651c02c7ecc 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -346,44 +346,85 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe 'Variables' do
let(:trigger_request) { create(:ci_trigger_request) }
+ let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
- let(:job) do
- create :ci_build, pipeline: pipeline, trigger_request: trigger_request
- end
+ context 'when user is a maintainer' do
+ shared_examples 'no reveal button variables behavior' do
+ it 'renders a hidden value with no reveal values button', :js do
+ expect(page).to have_content('Token')
+ expect(page).to have_content('Variables')
+
+ expect(page).not_to have_css('.js-reveal-variables')
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: '••••••')
+ end
+ end
+
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+
+ visit project_job_path(project, job)
+ end
+
+ it_behaves_like 'no reveal button variables behavior'
+ end
- shared_examples 'expected variables behavior' do
- it 'shows variable key and value after click', :js do
- expect(page).to have_content('Token')
- expect(page).to have_css('.js-reveal-variables')
- expect(page).not_to have_css('.js-build-variable')
- expect(page).not_to have_css('.js-build-value')
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
- click_button 'Reveal Variables'
+ visit project_job_path(project, job)
+ end
- expect(page).not_to have_css('.js-reveal-variables')
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ it_behaves_like 'no reveal button variables behavior'
end
end
- context 'when variables are stored in trigger_request' do
+ context 'when user is a maintainer' do
before do
- trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ project.add_maintainer(user)
+ end
- visit project_job_path(project, job)
+ shared_examples 'reveal button variables behavior' do
+ it 'renders a hidden value with a reveal values button', :js do
+ expect(page).to have_content('Token')
+ expect(page).to have_content('Variables')
+
+ expect(page).to have_css('.js-reveal-variables')
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: '••••••')
+ end
+
+ it 'reveals values on button click', :js do
+ click_button 'Reveal values'
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ end
end
- it_behaves_like 'expected variables behavior'
- end
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
- context 'when variables are stored in pipeline_variables' do
- before do
- create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+ visit project_job_path(project, job)
+ end
- visit project_job_path(project, job)
+ it_behaves_like 'reveal button variables behavior'
end
- it_behaves_like 'expected variables behavior'
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+
+ visit project_job_path(project, job)
+ end
+
+ it_behaves_like 'reveal button variables behavior'
+ end
end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 996040fde02..055a0c83a11 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -115,6 +115,21 @@ describe 'Prioritize labels' do
end
end
+ it 'user can see a primary button when there are only prioritized labels', :js do
+ visit project_labels_path(project)
+
+ page.within('.other-labels') do
+ all('.js-toggle-priority').each do |el|
+ el.click
+ end
+ wait_for_requests
+ end
+
+ page.within('.breadcrumbs-container') do
+ expect(page).to have_link('New label')
+ end
+ end
+
it 'shows a help message about prioritized labels' do
visit project_labels_path(project)
diff --git a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
index 227bdf524fe..8ba91fe7fd7 100644
--- a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
@@ -10,54 +10,9 @@ describe 'Projects > Show > Developer views empty project instructions' do
sign_in(developer)
end
- context 'without an SSH key' do
- it 'defaults to HTTP' do
- visit_project
-
- expect_instructions_for('http')
- end
-
- it 'switches to SSH', :js do
- visit_project
-
- select_protocol('SSH')
-
- expect_instructions_for('ssh')
- end
- end
-
- context 'with an SSH key' do
- before do
- create(:personal_key, user: developer)
- end
-
- it 'defaults to SSH' do
- visit_project
-
- expect_instructions_for('ssh')
- end
-
- it 'switches to HTTP', :js do
- visit_project
-
- select_protocol('HTTP')
-
- expect_instructions_for('http')
- end
- end
-
- def visit_project
+ it 'displays "git clone" instructions' do
visit project_path(project)
- end
-
- def select_protocol(protocol)
- find('#clone-dropdown').click
- find(".#{protocol.downcase}-selector").click
- end
-
- def expect_instructions_for(protocol)
- msg = :"#{protocol.downcase}_url_to_repo"
- expect(page).to have_content("git clone #{project.send(msg)}")
+ expect(page).to have_content("git clone")
end
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 546619e88ec..88f3397608f 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -8,13 +8,18 @@ describe 'Projects > Show > User manages notifications', :js do
visit project_path(project)
end
- it 'changes the notification setting' do
+ def click_notifications_button
first('.notifications-btn').click
+ end
+
+ it 'changes the notification setting' do
+ click_notifications_button
click_link 'On mention'
- page.within '#notifications-button' do
- expect(page).to have_content 'On mention'
- end
+ wait_for_requests
+
+ click_notifications_button
+ expect(find('.update-notification.is-active')).to have_content('On mention')
end
context 'custom notification settings' do
@@ -38,7 +43,7 @@ describe 'Projects > Show > User manages notifications', :js do
end
it 'shows notification settings checkbox' do
- first('.notifications-btn').click
+ click_notifications_button
page.find('a[data-notification-level="custom"]').click
page.within('.custom-notifications-form') do
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 7b3711531c6..24777788248 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -21,18 +21,6 @@ describe 'Projects > Show > Collaboration links' do
end
end
- # The project header
- page.within('.project-home-panel') do
- aggregate_failures 'dropdown links in the project home panel' do
- expect(page).to have_link('New issue')
- expect(page).to have_link('New merge request')
- expect(page).to have_link('New snippet')
- expect(page).to have_link('New file')
- expect(page).to have_link('New branch')
- expect(page).to have_link('New tag')
- end
- end
-
# The dropdown above the tree
page.within('.repo-breadcrumb') do
aggregate_failures 'dropdown links above the repo tree' do
@@ -61,17 +49,6 @@ describe 'Projects > Show > Collaboration links' do
end
end
- page.within('.project-home-panel') do
- aggregate_failures 'dropdown links' do
- expect(page).not_to have_link('New issue')
- expect(page).not_to have_link('New merge request')
- expect(page).not_to have_link('New snippet')
- expect(page).not_to have_link('New file')
- expect(page).not_to have_link('New branch')
- expect(page).not_to have_link('New tag')
- end
- end
-
page.within('.repo-breadcrumb') do
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New file')
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index 9a82fee1b5d..ffa80235083 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -29,7 +29,7 @@ describe 'Projects > Show > User sees Git instructions' do
expect(element.text).to include(project.http_url_to_repo)
end
- expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
@@ -41,7 +41,7 @@ describe 'Projects > Show > User sees Git instructions' do
expect(page).to have_content(project.title)
end
- expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index df2b492ae6b..dcca1d388c7 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -21,7 +21,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -30,7 +30,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" button not linked' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_text('Auto DevOps enabled')
end
end
@@ -45,19 +45,19 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it '"New file" button linked to new file page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('New file', href: project_new_blob_path(project, project.default_branch || 'master'))
end
end
- it '"Add Readme" button linked to new file populated for a readme' do
- page.within('.project-stats') do
- expect(page).to have_link('Add Readme', href: presenter.add_readme_path)
+ it '"Add README" button linked to new file populated for a README' do
+ page.within('.project-buttons') do
+ expect(page).to have_link('Add README', href: presenter.add_readme_path)
end
end
it '"Add license" button linked to new file populated for a license' do
- page.within('.project-metadata') do
+ page.within('.project-stats') do
expect(page).to have_link('Add license', href: presenter.add_license_path)
end
end
@@ -67,7 +67,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" anchor linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -77,7 +77,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
let(:project) { create(:project, :public, :empty_repo, auto_devops_attributes: { enabled: false }) }
it '"Enable Auto DevOps" button linked to settings page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -86,7 +86,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Kubernetes cluster button' do
it '"Add Kubernetes cluster" button linked to clusters page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
end
end
@@ -96,7 +96,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
end
end
@@ -119,7 +119,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" button not linked' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_text('Auto DevOps enabled')
end
end
@@ -129,14 +129,14 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
let(:project) { create(:project, :public, :repository, auto_devops_attributes: { enabled: false }) }
it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
end
it 'no Kubernetes cluster button if can not manage clusters' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Add Kubernetes cluster')
expect(page).not_to have_link('Kubernetes configured')
end
@@ -151,59 +151,59 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
sign_in(user)
end
- context 'Readme button' do
+ context 'README button' do
before do
allow(Project).to receive(:find_by_full_path)
.with(project.full_path, follow_redirects: true)
.and_return(project)
end
- context 'when the project has a populated Readme' do
- it 'show the "Readme" anchor' do
+ context 'when the project has a populated README' do
+ it 'show the "README" anchor' do
visit project_path(project)
expect(project.repository.readme).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
- expect(page).to have_link('Readme', href: presenter.readme_path)
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add README', href: presenter.add_readme_path)
+ expect(page).to have_link('README', href: presenter.readme_path)
end
end
- context 'when the project has an empty Readme' do
- it 'show the "Readme" anchor' do
+ context 'when the project has an empty README' do
+ it 'show the "README" anchor' do
allow(project.repository).to receive(:readme).and_return(fake_blob(path: 'README.md', data: '', size: 0))
visit project_path(project)
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
- expect(page).to have_link('Readme', href: presenter.readme_path)
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add README', href: presenter.add_readme_path)
+ expect(page).to have_link('README', href: presenter.readme_path)
end
end
end
end
- context 'when the project does not have a Readme' do
- it 'shows the "Add Readme" button' do
+ context 'when the project does not have a README' do
+ it 'shows the "Add README" button' do
allow(project.repository).to receive(:readme).and_return(nil)
visit project_path(project)
- page.within('.project-stats') do
- expect(page).to have_link('Add Readme', href: presenter.add_readme_path)
+ page.within('.project-buttons') do
+ expect(page).to have_link('Add README', href: presenter.add_readme_path)
end
end
end
end
- it 'no "Add Changelog" button if the project already has a changelog' do
+ it 'no "Add CHANGELOG" button if the project already has a changelog' do
visit project_path(project)
expect(project.repository.changelog).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Changelog')
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add CHANGELOG')
end
end
@@ -212,18 +212,18 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.license_blob).not_to be_nil
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Add license')
end
end
- it 'no "Add Contribution guide" button if the project already has a contribution guide' do
+ it 'no "Add CONTRIBUTING" button if the project already has a contribution guide' do
visit project_path(project)
expect(project.repository.contribution_guide).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Contribution guide')
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add CONTRIBUTING')
end
end
@@ -232,7 +232,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Set up CI/CD')
end
end
@@ -246,7 +246,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.gitlab_ci_yml).to be_nil
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path)
end
end
@@ -266,7 +266,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Set up CI/CD')
end
end
@@ -278,7 +278,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" anchor linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -290,7 +290,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Enable Auto DevOps" button linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -302,7 +302,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(page).to have_selector('.js-autodevops-banner')
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -323,7 +323,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -335,7 +335,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Add Kubernetes cluster" button linked to clusters page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
end
end
@@ -345,7 +345,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
end
end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 3f4fe549f3e..36cfeb5ed84 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -13,7 +13,7 @@ describe 'Maintainer views tags' do
before do
visit project_path(project)
- click_on 'Add Readme'
+ click_on 'Add README'
fill_in :commit_message, with: 'Add a README file', visible: true
click_button 'Commit changes'
visit project_tags_path(project)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 515f6f70b99..80f7232f282 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -640,4 +640,131 @@ describe IssuesFinder do
end
end
end
+
+ describe '#use_subquery_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
+ end
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the database is not Postgres' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the attempt_group_search_optimizations param is falsey' do
+ let(:params) { { search: 'foo' } }
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the use_subquery_for_group_issues_search flag is disabled' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_subquery_for_group_issues_search: false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_subquery_for_search?).to be_truthy
+ end
+ end
+ end
+
+ describe '#use_cte_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ stub_feature_flags(use_cte_for_group_issues_search: true)
+ stub_feature_flags(use_subquery_for_group_issues_search: false)
+ end
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the database is not Postgres' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the attempt_group_search_optimizations param is falsey' do
+ let(:params) { { search: 'foo' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the use_cte_for_group_issues_search flag is disabled' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_cte_for_group_issues_search: false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when use_subquery_for_search? is true' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index ccef17a6615..3d9e0628f63 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -32,7 +32,8 @@
},
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] },
- "hostname": { "type": ["string", "null"] }
+ "hostname": { "type": ["string", "null"] },
+ "email": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/job/trigger.json b/spec/fixtures/api/schemas/job/trigger.json
index 1c7e9cc7693..807178c662c 100644
--- a/spec/fixtures/api/schemas/job/trigger.json
+++ b/spec/fixtures/api/schemas/job/trigger.json
@@ -12,12 +12,11 @@
"type": "object",
"required": [
"key",
- "value",
"public"
],
"properties": {
"key": { "type": "string" },
- "value": { "type": "string" },
+ "value": { "type": "string", "optional": true },
"public": { "type": "boolean" }
},
"additionalProperties": false
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
new file mode 100644
index 00000000000..af54a777373
--- /dev/null
+++ b/spec/initializers/lograge_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'lograge', type: :request do
+ let(:headers) { { 'X-Request-ID' => 'new-correlation-id' } }
+
+ context 'for API requests' do
+ subject { get("/api/v4/endpoint", {}, headers) }
+
+ it 'logs to api_json log' do
+ # we assert receiving parameters by grape logger
+ expect_any_instance_of(Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp).to receive(:call)
+ .with(anything, anything, anything, a_hash_including("correlation_id" => "new-correlation-id"))
+ .and_call_original
+
+ subject
+ end
+ end
+
+ context 'for Controller requests' do
+ subject { get("/", {}, headers) }
+
+ it 'logs to production_json log' do
+ # formatter receives a hash with correlation id
+ expect(Lograge.formatter).to receive(:call)
+ .with(a_hash_including("correlation_id" => "new-correlation-id"))
+ .and_call_original
+
+ # a log file receives a line with correlation id
+ expect(Lograge.logger).to receive(:send)
+ .with(anything, include('"correlation_id":"new-correlation-id"'))
+ .and_call_original
+
+ subject
+ end
+ end
+end
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index e46edec9abb..14ef1193984 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -176,6 +176,54 @@ describe('Applications', () => {
});
});
+ describe('Cert-Manager application', () => {
+ describe('when not installed', () => {
+ it('renders email & allows editing', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'before@example.com',
+ status: 'installable',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('before@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toBe(null);
+ });
+ });
+
+ describe('when installed', () => {
+ it('renders email in readonly', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'after@example.com',
+ status: 'installed',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('after@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+ });
+
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 540d7f30858..3c3d9977ffb 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -42,6 +42,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
@@ -86,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 7ea0878ad45..1ca55549094 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -115,6 +115,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[6].status_reason,
requestStatus: null,
requestReason: null,
+ email: mockResponseData.applications[6].email,
},
},
});
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index 5ffe5a366ba..44313caba29 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -489,8 +489,6 @@ export default {
diff_discussion: true,
truncated_diff_lines:
'<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
- image_diff_html:
- '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
};
export const imageDiffDiscussions = [
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 4b339a0553f..55ce19927e0 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -29,6 +29,7 @@ import actions, {
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import mockDiffFile from 'spec/diffs/mock_data/diff_file';
import testAction from '../../helpers/vuex_action_helper';
describe('DiffsStoreActions', () => {
@@ -607,11 +608,18 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- beforeEach(() => {
- spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData');
- });
-
it('dispatches actions', done => {
+ const commitId = 'something';
+ const formData = {
+ diffFile: { ...mockDiffFile },
+ noteableData: {},
+ };
+ const note = {};
+ const state = {
+ commit: {
+ id: commitId,
+ },
+ };
const dispatch = jasmine.createSpy('dispatch').and.callFake(name => {
switch (name) {
case 'saveNote':
@@ -625,11 +633,19 @@ describe('DiffsStoreActions', () => {
}
});
- saveDiffDiscussion({ dispatch }, { note: {}, formData: {} })
+ saveDiffDiscussion({ state, dispatch }, { note, formData })
.then(() => {
- expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]);
- expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
- expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
+ const { calls } = dispatch;
+
+ expect(calls.count()).toBe(5);
+ expect(calls.argsFor(0)).toEqual(['saveNote', jasmine.any(Object), { root: true }]);
+
+ const postData = calls.argsFor(0)[1];
+
+ expect(postData.data.note.commit_id).toBe(commitId);
+
+ expect(calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
+ expect(calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index 717f983da65..f096638e3d6 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -150,7 +150,7 @@ describe('DiffsStoreUtils', () => {
note: {
noteable_type: options.noteableType,
noteable_id: options.noteableData.id,
- commit_id: '',
+ commit_id: undefined,
type: DIFF_NOTE_TYPE,
line_code: options.noteTargetLine.line_code,
note: options.note,
@@ -209,7 +209,7 @@ describe('DiffsStoreUtils', () => {
note: {
noteable_type: options.noteableType,
noteable_id: options.noteableData.id,
- commit_id: '',
+ commit_id: undefined,
type: LEGACY_DIFF_NOTE_TYPE,
line_code: options.noteTargetLine.line_code,
note: options.note,
diff --git a/spec/javascripts/jobs/components/trigger_block_spec.js b/spec/javascripts/jobs/components/trigger_block_spec.js
index 7254851a9e7..448197b82c0 100644
--- a/spec/javascripts/jobs/components/trigger_block_spec.js
+++ b/spec/javascripts/jobs/components/trigger_block_spec.js
@@ -31,8 +31,8 @@ describe('Trigger block', () => {
});
describe('with variables', () => {
- describe('reveal variables', () => {
- it('reveals variables on click', done => {
+ describe('hide/reveal variables', () => {
+ it('should toggle variables on click', done => {
vm = mountComponent(Component, {
trigger: {
short_token: 'bd7e',
@@ -48,6 +48,10 @@ describe('Trigger block', () => {
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Hide values',
+ );
+
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
'UPLOAD_TO_GCS',
);
@@ -58,6 +62,26 @@ describe('Trigger block', () => {
);
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true');
+
+ vm.$el.querySelector('.js-reveal-variables').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Reveal values',
+ );
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_GCS',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_S3',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/lib/utils/dom_utils_spec.js b/spec/javascripts/lib/utils/dom_utils_spec.js
index 1fb2e4584a0..2bcf37f35c7 100644
--- a/spec/javascripts/lib/utils/dom_utils_spec.js
+++ b/spec/javascripts/lib/utils/dom_utils_spec.js
@@ -1,4 +1,6 @@
-import { addClassIfElementExists } from '~/lib/utils/dom_utils';
+import { addClassIfElementExists, canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+
+const TEST_MARGIN = 5;
describe('DOM Utils', () => {
describe('addClassIfElementExists', () => {
@@ -34,4 +36,54 @@ describe('DOM Utils', () => {
addClassIfElementExists(childElement, className);
});
});
+
+ describe('canScrollUp', () => {
+ [1, 100].forEach(scrollTop => {
+ it(`is true if scrollTop is > 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(true);
+ });
+ });
+
+ [0, -10].forEach(scrollTop => {
+ it(`is false if scrollTop is <= 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(false);
+ });
+ });
+
+ it('is true if scrollTop is > margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN + 1 }, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if scrollTop is <= margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN }, TEST_MARGIN)).toBe(false);
+ });
+ });
+
+ describe('canScrollDown', () => {
+ let element;
+
+ beforeEach(() => {
+ element = { scrollTop: 7, offsetHeight: 22, scrollHeight: 30 };
+ });
+
+ it('is true if element can be scrolled down', () => {
+ expect(canScrollDown(element)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down', () => {
+ element.scrollHeight -= 1;
+
+ expect(canScrollDown(element)).toBe(false);
+ });
+
+ it('is true if element can be scrolled down, with margin given', () => {
+ element.scrollHeight += TEST_MARGIN;
+
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down, with margin given', () => {
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(false);
+ });
+ });
});
diff --git a/spec/lib/banzai/filter/front_matter_filter_spec.rb b/spec/lib/banzai/filter/front_matter_filter_spec.rb
new file mode 100644
index 00000000000..3071dc7cf21
--- /dev/null
+++ b/spec/lib/banzai/filter/front_matter_filter_spec.rb
@@ -0,0 +1,140 @@
+require 'rails_helper'
+
+describe Banzai::Filter::FrontMatterFilter do
+ include FilterSpecHelper
+
+ it 'allows for `encoding:` before the front matter' do
+ content = <<~MD
+ # encoding: UTF-8
+ ---
+ foo: foo
+ bar: bar
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ expect(output).not_to match 'encoding'
+ end
+
+ it 'converts YAML front matter to a fenced code block' do
+ content = <<~MD
+ ---
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '---'
+ expect(output).to include "```yaml\nfoo: :foo_symbol\n"
+ end
+ end
+
+ it 'converts TOML frontmatter to a fenced code block' do
+ content = <<~MD
+ +++
+ foo = :foo_symbol
+ bar = :bar_symbol
+ +++
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '+++'
+ expect(output).to include "```toml\nfoo = :foo_symbol\n"
+ end
+ end
+
+ it 'converts JSON front matter to a fenced code block' do
+ content = <<~MD
+ ;;;
+ {
+ "foo": ":foo_symbol",
+ "bar": ":bar_symbol"
+ }
+ ;;;
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include ';;;'
+ expect(output).to include "```json\n{\n \"foo\": \":foo_symbol\",\n"
+ end
+ end
+
+ it 'converts arbitrary front matter to a fenced code block' do
+ content = <<~MD
+ ---arbitrary
+ foo = :foo_symbol
+ bar = :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '---arbitrary'
+ expect(output).to include "```arbitrary\nfoo = :foo_symbol\n"
+ end
+ end
+
+ context 'on content without front matter' do
+ it 'returns the content unmodified' do
+ content = <<~MD
+ # This is some Markdown
+
+ It has no YAML front matter to parse.
+ MD
+
+ expect(filter(content)).to eq content
+ end
+ end
+
+ context 'on front matter without content' do
+ it 'converts YAML front matter to a fenced code block' do
+ content = <<~MD
+ ---
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ---
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).to eq <<~MD
+ ```yaml
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ```
+
+ MD
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 91d4a60ba95..1a87cfa5b45 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -351,21 +351,50 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
context 'group context' do
- let(:context) { { project: nil, group: create(:group) } }
- let(:milestone) { create(:milestone, project: project) }
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
- it 'links to a valid reference' do
- reference = "#{project.full_path}%#{milestone.iid}"
+ context 'when project milestone' do
+ let(:milestone) { create(:milestone, project: project) }
- result = reference_filter("See #{reference}", context)
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}%#{milestone.iid}"
- expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ result = reference_filter("See #{reference}", context)
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See %#{milestone.iid}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
end
- it 'ignores internal references' do
- exp = act = "See %#{milestone.iid}"
+ context 'when group milestone' do
+ let(:group_milestone) { create(:milestone, title: 'group_milestone', group: group) }
- expect(reference_filter(act, context).to_html).to eq exp
+ context 'for subgroups', :nested_groups do
+ let(:sub_group) { create(:group, parent: group) }
+ let(:sub_group_milestone) { create(:milestone, title: 'sub_group_milestone', group: sub_group) }
+
+ it 'links to a valid reference of subgroup and group milestones' do
+ [group_milestone, sub_group_milestone].each do |milestone|
+ reference = "%#{milestone.title}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: sub_group })
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See %#{group_milestone.iid}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
end
end
diff --git a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
deleted file mode 100644
index 9f1b862ef19..00000000000
--- a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'rails_helper'
-
-describe Banzai::Filter::YamlFrontMatterFilter do
- include FilterSpecHelper
-
- it 'allows for `encoding:` before the frontmatter' do
- content = <<-MD.strip_heredoc
- # encoding: UTF-8
- ---
- foo: foo
- ---
-
- # Header
-
- Content
- MD
-
- output = filter(content)
-
- expect(output).not_to match 'encoding'
- end
-
- it 'converts YAML frontmatter to a fenced code block' do
- content = <<-MD.strip_heredoc
- ---
- bar: :bar_symbol
- ---
-
- # Header
-
- Content
- MD
-
- output = filter(content)
-
- aggregate_failures do
- expect(output).not_to include '---'
- expect(output).to include "```yaml\nbar: :bar_symbol\n```"
- end
- end
-
- context 'on content without frontmatter' do
- it 'returns the content unmodified' do
- content = <<-MD.strip_heredoc
- # This is some Markdown
-
- It has no YAML frontmatter to parse.
- MD
-
- expect(filter(content)).to eq content
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb
new file mode 100644
index 00000000000..b6c1edbbf8b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::BackfillHashedProjectRepositories, :migration, schema: 20181130102132 do
+ let(:namespaces) { table(:namespaces) }
+ let(:project_repositories) { table(:project_repositories) }
+ let(:projects) { table(:projects) }
+ let(:shards) { table(:shards) }
+ let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:shard) { shards.create!(name: 'default') }
+
+ describe described_class::ShardFinder do
+ describe '#find_shard_id' do
+ it 'creates a new shard when it does not exist yet' do
+ expect { subject.find_shard_id('other') }.to change(shards, :count).by(1)
+ end
+
+ it 'returns the shard when it exists' do
+ shards.create(id: 5, name: 'other')
+
+ shard_id = subject.find_shard_id('other')
+
+ expect(shard_id).to eq(5)
+ end
+
+ it 'only queries the database once to retrieve shards' do
+ subject.find_shard_id('default')
+
+ expect { subject.find_shard_id('default') }.not_to exceed_query_limit(0)
+ end
+ end
+ end
+
+ describe described_class::Project do
+ describe '.on_hashed_storage' do
+ it 'finds projects with repository on hashed storage' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 2)
+ projects.create!(id: 3, name: 'baz', path: 'baz', namespace_id: group.id, storage_version: 0)
+ projects.create!(id: 4, name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: nil)
+
+ expect(described_class.on_hashed_storage.pluck(:id)).to match_array([1, 2])
+ end
+ end
+
+ describe '.without_project_repository' do
+ it 'finds projects which do not have a projects_repositories entry' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id)
+ project_repositories.create!(project_id: 2, disk_path: '@phony/foo/bar', shard_id: shard.id)
+
+ expect(described_class.without_project_repository.pluck(:id)).to contain_exactly(1)
+ end
+ end
+ end
+
+ describe '#perform' do
+ it 'creates a project_repository row for projects on hashed storage that need one' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 2)
+
+ expect { described_class.new.perform(1, projects.last.id) }.to change(project_repositories, :count).by(2)
+ end
+
+ it 'does nothing for projects on hashed storage that have already a project_repository row' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ project_repositories.create!(project_id: 1, disk_path: '@phony/foo/bar', shard_id: shard.id)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ end
+
+ it 'does nothing for projects on legacy storage' do
+ projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 0)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ end
+
+ it 'inserts rows in a single query' do
+ projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+
+ control_count = ActiveRecord::QueryRecorder.new { described_class.new.perform(1, projects.last.id) }
+
+ projects.create!(name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+ projects.create!(name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb b/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb
new file mode 100644
index 00000000000..1e969542975
--- /dev/null
+++ b/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BranchPushMergeCommitAnalyzer do
+ let(:project) { create(:project, :repository) }
+ let(:oldrev) { 'merge-commit-analyze-before' }
+ let(:newrev) { 'merge-commit-analyze-after' }
+ let(:commits) { project.repository.commits_between(oldrev, newrev).reverse }
+
+ subject { described_class.new(commits) }
+
+ describe '#get_merge_commit' do
+ let(:expected_merge_commits) do
+ {
+ '646ece5cfed840eca0a4feb21bcd6a81bb19bda3' => '646ece5cfed840eca0a4feb21bcd6a81bb19bda3',
+ '29284d9bcc350bcae005872d0be6edd016e2efb5' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '5f82584f0a907f3b30cfce5bb8df371454a90051' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '8a994512e8c8f0dfcf22bb16df6e876be7a61036' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '689600b91aabec706e657e38ea706ece1ee8268f' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ 'db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9' => 'db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9'
+ }
+ end
+
+ it 'returns correct merge commit SHA for each commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+
+ context 'when one parent has two children' do
+ let(:oldrev) { '1adbdefe31288f3bbe4b614853de4908a0b6f792' }
+ let(:newrev) { '5f82584f0a907f3b30cfce5bb8df371454a90051' }
+
+ let(:expected_merge_commits) do
+ {
+ '5f82584f0a907f3b30cfce5bb8df371454a90051' => '5f82584f0a907f3b30cfce5bb8df371454a90051',
+ '8a994512e8c8f0dfcf22bb16df6e876be7a61036' => '5f82584f0a907f3b30cfce5bb8df371454a90051',
+ '689600b91aabec706e657e38ea706ece1ee8268f' => '689600b91aabec706e657e38ea706ece1ee8268f'
+ }
+ end
+
+ it 'returns correct merge commit SHA for each commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+ end
+
+ context 'when relevant_commit_ids is provided' do
+ let(:relevant_commit_id) { '8a994512e8c8f0dfcf22bb16df6e876be7a61036' }
+ subject { described_class.new(commits, relevant_commit_ids: [relevant_commit_id]) }
+
+ it 'returns correct merge commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ subject = described_class.new(commits, relevant_commit_ids: [commit])
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/correlation_id_spec.rb b/spec/lib/gitlab/correlation_id_spec.rb
new file mode 100644
index 00000000000..584d1f48386
--- /dev/null
+++ b/spec/lib/gitlab/correlation_id_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::CorrelationId do
+ describe '.use_id' do
+ it 'yields when executed' do
+ expect { |blk| described_class.use_id('id', &blk) }.to yield_control
+ end
+
+ it 'stacks correlation ids' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do |current_id|
+ expect(current_id).to eq('id2')
+ end
+ end
+ end
+
+ it 'for missing correlation id it generates random one' do
+ described_class.use_id('id1') do
+ described_class.use_id(nil) do |current_id|
+ expect(current_id).not_to be_empty
+ expect(current_id).not_to eq('id1')
+ end
+ end
+ end
+ end
+
+ describe '.current_id' do
+ subject { described_class.current_id }
+
+ it 'returns last correlation id' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do
+ is_expected.to eq('id2')
+ end
+ end
+ end
+ end
+
+ describe '.current_or_new_id' do
+ subject { described_class.current_or_new_id }
+
+ context 'when correlation id is set' do
+ it 'returns last correlation id' do
+ described_class.use_id('id1') do
+ is_expected.to eq('id1')
+ end
+ end
+ end
+
+ context 'when correlation id is missing' do
+ it 'returns a new correlation id' do
+ expect(described_class).to receive(:new_id)
+ .and_call_original
+
+ is_expected.not_to be_empty
+ end
+ end
+ end
+
+ describe '.ids' do
+ subject { described_class.send(:ids) }
+
+ it 'returns empty list if not correlation is used' do
+ is_expected.to be_empty
+ end
+
+ it 'returns list if correlation ids are used' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do
+ is_expected.to eq(%w(id1 id2))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
new file mode 100644
index 00000000000..363c2aa67af
--- /dev/null
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Git::ObjectPool do
+ let(:pool_repository) { create(:pool_repository) }
+ let(:source_repository) { pool_repository.source_project.repository }
+
+ subject { pool_repository.object_pool }
+
+ describe '#storage' do
+ it "equals the pool repository's shard name" do
+ expect(subject.storage).not_to be_nil
+ expect(subject.storage).to eq(pool_repository.shard_name)
+ end
+ end
+
+ describe '#create' do
+ before do
+ subject.create
+ end
+
+ context "when the pool doesn't exist yet" do
+ it 'creates the pool' do
+ expect(subject.exists?).to be(true)
+ end
+ end
+
+ context 'when the pool already exists' do
+ it 'raises an FailedPrecondition' do
+ expect do
+ subject.create
+ end.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
+
+ describe '#exists?' do
+ context "when the object pool doesn't exist" do
+ it 'returns false' do
+ expect(subject.exists?).to be(false)
+ end
+ end
+
+ context 'when the object pool exists' do
+ let(:pool) { create(:pool_repository, :ready) }
+
+ subject { pool.object_pool }
+
+ it 'returns true' do
+ expect(subject.exists?).to be(true)
+ end
+ end
+ end
+
+ describe '#link' do
+ let!(:pool_repository) { create(:pool_repository, :ready) }
+
+ context 'when no remotes are set' do
+ it 'sets a remote' do
+ subject.link(source_repository)
+
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Rugged::Repository.new(subject.repository.path)
+ end
+
+ expect(repo.remotes.count).to be(1)
+ expect(repo.remotes.first.name).to eq(source_repository.object_pool_remote_name)
+ end
+ end
+
+ context 'when the remote is already set' do
+ before do
+ subject.link(source_repository)
+ end
+
+ it "doesn't raise an error" do
+ subject.link(source_repository)
+
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Rugged::Repository.new(subject.repository.path)
+ end
+
+ expect(repo.remotes.count).to be(1)
+ expect(repo.remotes.first.name).to eq(source_repository.object_pool_remote_name)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
new file mode 100644
index 00000000000..149b7ec5bb0
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::ObjectPoolService do
+ let(:pool_repository) { create(:pool_repository) }
+ let(:project) { create(:project, :repository) }
+ let(:raw_repository) { project.repository.raw }
+ let(:object_pool) { pool_repository.object_pool }
+
+ subject { described_class.new(object_pool) }
+
+ before do
+ subject.create(raw_repository)
+ end
+
+ describe '#create' do
+ it 'exists on disk' do
+ expect(object_pool.repository.exists?).to be(true)
+ end
+
+ context 'when the pool already exists' do
+ it 'returns an error' do
+ expect do
+ subject.create(raw_repository)
+ end.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
+
+ describe '#delete' do
+ it 'removes the repository from disk' do
+ subject.delete
+
+ expect(object_pool.repository.exists?).to be(false)
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ subject.delete
+
+ expect { object_pool.delete }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7df129da95a..bae5b21c26f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -287,6 +287,7 @@ project:
- statistics
- container_repositories
- uploads
+- file_uploads
- import_state
- members_and_requesters
- build_trace_section_names
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index 0a62785f880..cff7dd58c8c 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -7,6 +7,10 @@ describe Gitlab::JsonLogger do
let(:now) { Time.now }
describe '#format_message' do
+ before do
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ end
+
it 'formats strings' do
output = subject.format_message('INFO', now, 'test', 'Hello world')
data = JSON.parse(output)
@@ -14,6 +18,7 @@ describe Gitlab::JsonLogger do
expect(data['severity']).to eq('INFO')
expect(data['time']).to eq(now.utc.iso8601(3))
expect(data['message']).to eq('Hello world')
+ expect(data['correlation_id']).to eq('new-correlation-id')
end
it 'formats hashes' do
@@ -24,6 +29,7 @@ describe Gitlab::JsonLogger do
expect(data['time']).to eq(now.utc.iso8601(3))
expect(data['hello']).to eq(1)
expect(data['message']).to be_nil
+ expect(data['correlation_id']).to eq('new-correlation-id')
end
end
end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index d3b41b27b80..1128eaf8560 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -19,14 +19,15 @@ describe Gitlab::Sentry do
end
it 'raises the exception if it should' do
- expect(described_class).to receive(:should_raise?).and_return(true)
+ expect(described_class).to receive(:should_raise_for_dev?).and_return(true)
expect { described_class.track_exception(exception) }
.to raise_error(RuntimeError)
end
context 'when exceptions should not be raised' do
before do
- allow(described_class).to receive(:should_raise?).and_return(false)
+ allow(described_class).to receive(:should_raise_for_dev?).and_return(false)
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'logs the exception with all attributes passed' do
@@ -35,8 +36,14 @@ describe Gitlab::Sentry do
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
}
+ expected_tags = {
+ correlation_id: 'cid'
+ }
+
expect(Raven).to receive(:capture_exception)
- .with(exception, extra: a_hash_including(expected_extras))
+ .with(exception,
+ tags: a_hash_including(expected_tags),
+ extra: a_hash_including(expected_extras))
described_class.track_exception(
exception,
@@ -58,6 +65,7 @@ describe Gitlab::Sentry do
before do
allow(described_class).to receive(:enabled?).and_return(true)
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'calls Raven.capture_exception' do
@@ -66,8 +74,14 @@ describe Gitlab::Sentry do
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
}
+ expected_tags = {
+ correlation_id: 'cid'
+ }
+
expect(Raven).to receive(:capture_exception)
- .with(exception, extra: a_hash_including(expected_extras))
+ .with(exception,
+ tags: a_hash_including(expected_tags),
+ extra: a_hash_including(expected_extras))
described_class.track_acceptable_exception(
exception,
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 2421b1e5a1a..f773f370ee2 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
"queue_namespace" => "cronjob",
"jid" => "da883554ee4fe414012f5f42",
"created_at" => timestamp.to_f,
- "enqueued_at" => timestamp.to_f
+ "enqueued_at" => timestamp.to_f,
+ "correlation_id" => 'cid'
}
end
let(:logger) { double() }
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
new file mode 100644
index 00000000000..a138ad7c910
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::CorrelationInjector do
+ class TestWorker
+ include ApplicationWorker
+ end
+
+ before do |example|
+ Sidekiq.client_middleware do |chain|
+ chain.add described_class
+ end
+ end
+
+ after do |example|
+ Sidekiq.client_middleware do |chain|
+ chain.remove described_class
+ end
+
+ Sidekiq::Queues.clear_all
+ end
+
+ around do |example|
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+
+ it 'injects into payload the correlation id' do
+ expect_any_instance_of(described_class).to receive(:call).and_call_original
+
+ Gitlab::CorrelationId.use_id('new-correlation-id') do
+ TestWorker.perform_async(1234)
+ end
+
+ expected_job_params = {
+ "class" => "TestWorker",
+ "args" => [1234],
+ "correlation_id" => "new-correlation-id"
+ }
+
+ expect(Sidekiq::Queues.jobs_by_worker).to a_hash_including(
+ "TestWorker" => a_collection_containing_exactly(
+ a_hash_including(expected_job_params)))
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
new file mode 100644
index 00000000000..94ae4ffa184
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::CorrelationLogger do
+ class TestWorker
+ include ApplicationWorker
+ end
+
+ before do |example|
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.add described_class
+ end
+ end
+
+ after do |example|
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.remove described_class
+ end
+ end
+
+ it 'injects into payload the correlation id' do
+ expect_any_instance_of(described_class).to receive(:call).and_call_original
+
+ expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
+ expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id')
+ end
+
+ Sidekiq::Client.push(
+ 'queue' => 'test',
+ 'class' => TestWorker,
+ 'args' => [1234],
+ 'correlation_id' => 'new-correlation-id')
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 77b07cf1ac9..35415030154 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -20,7 +20,7 @@ describe Appearance do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', false do
+ it_behaves_like 'model with uploads', false do
let(:model_object) { create(:appearance, :with_logo) }
let(:upload_attribute) { :logo }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4cdcae5f670..89f78f629d4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1925,7 +1925,7 @@ describe Ci::Build do
context 'when token is empty' do
before do
- build.token = nil
+ build.update_columns(token: nil, token_encrypted: nil)
end
it { is_expected.to be_nil}
@@ -2141,7 +2141,7 @@ describe Ci::Build do
end
before do
- build.token = 'my-token'
+ build.set_token('my-token')
build.yaml_variables = []
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 97e50809647..47daa79873e 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -18,7 +18,7 @@ describe Clusters::Applications::Runner do
let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') }
it 'updates the application version' do
- expect(application.reload.version).to eq('0.1.38')
+ expect(application.reload.version).to eq('0.1.39')
end
end
end
@@ -46,7 +46,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.1.38')
+ expect(subject.version).to eq('0.1.39')
expect(subject).not_to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -64,7 +64,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.1.38')
+ expect(subject.version).to eq('0.1.39')
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 2a0039a0635..a2d2d77746d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -204,7 +204,7 @@ describe Commit do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message)
- expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…')
+ expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id...')
end
it "truncates a message with a newline before 80 characters at the newline" do
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 8cd129dc851..73eb7a1160d 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -12,6 +12,34 @@ describe DiscussionOnDiff do
expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
end
+
+ context 'with truncated diff lines diff limit set' do
+ let(:truncated_lines) do
+ subject.truncated_diff_lines(
+ diff_limit: diff_limit
+ )
+ end
+
+ context 'when diff limit is higher than default' do
+ let(:diff_limit) { DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES + 1 }
+
+ it 'returns fewer lines than the default' do
+ expect(subject.diff_lines.count).to be > diff_limit
+
+ expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ context 'when diff_limit is lower than default' do
+ let(:diff_limit) { 3 }
+
+ it 'returns fewer lines than the default' do
+ expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+
+ expect(truncated_lines.count).to be <= diff_limit
+ end
+ end
+ end
end
context "when some diff lines are meta" do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 0cdf430e9ab..55d83bc3a6b 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -351,3 +351,89 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do
end
end
end
+
+describe Ci::Build, 'TokenAuthenticatable' do
+ let(:token_field) { :token }
+ let(:build) { FactoryBot.build(:ci_build) }
+
+ it_behaves_like 'TokenAuthenticatable'
+
+ describe 'generating new token' do
+ context 'token is not generated yet' do
+ describe 'token field accessor' do
+ it 'makes it possible to access token' do
+ expect(build.token).to be_nil
+
+ build.save!
+
+ expect(build.token).to be_present
+ end
+ end
+
+ describe "ensure_token" do
+ subject { build.ensure_token }
+
+ it { is_expected.to be_a String }
+ it { is_expected.not_to be_blank }
+
+ it 'does not persist token' do
+ expect(build).not_to be_persisted
+ end
+ end
+
+ describe 'ensure_token!' do
+ it 'persists a new token' do
+ expect(build.ensure_token!).to eq build.reload.token
+ expect(build).to be_persisted
+ end
+
+ it 'persists new token as an encrypted string' do
+ build.ensure_token!
+
+ encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
+
+ expect(build.read_attribute('token_encrypted')).to eq encrypted
+ end
+
+ it 'does not persist a token in a clear text' do
+ build.ensure_token!
+
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ it 'persists a new token' do
+ build.save!
+
+ build.token.yield_self do |previous_token|
+ build.reset_token!
+
+ expect(build.token).not_to eq previous_token
+ expect(build.token).to be_a String
+ end
+ end
+ end
+ end
+
+ describe 'setting a new token' do
+ subject { build.set_token('0123456789') }
+
+ it 'returns the token' do
+ expect(subject).to eq '0123456789'
+ end
+
+ it 'writes a new encrypted token' do
+ expect(build.read_attribute('token_encrypted')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token_encrypted')).to be_present
+ end
+
+ it 'does not write a new cleartext token' do
+ expect(build.read_attribute('token')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 87aa5a46c21..e63881242f6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -739,7 +739,7 @@ describe Group do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', true do
+ it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 96561dab1c9..18b54cce834 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -249,7 +249,7 @@ describe Namespace do
move_dir_result
end
- expect(Gitlab::Sentry).to receive(:should_raise?).and_return(false) # like prod
+ expect(Gitlab::Sentry).to receive(:should_raise_for_dev?).and_return(false) # like prod
namespace.update(path: namespace.full_path + '_new')
end
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 541e78507e5..3d3878b8c39 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe PoolRepository do
describe 'associations' do
it { is_expected.to belong_to(:shard) }
+ it { is_expected.to have_one(:source_project) }
it { is_expected.to have_many(:member_projects) }
end
@@ -12,15 +13,14 @@ describe PoolRepository do
let!(:pool_repository) { create(:pool_repository) }
it { is_expected.to validate_presence_of(:shard) }
+ it { is_expected.to validate_presence_of(:source_project) }
end
describe '#disk_path' do
it 'sets the hashed disk_path' do
pool = create(:pool_repository)
- elements = File.split(pool.disk_path)
-
- expect(elements).to all( match(/\d{2,}/) )
+ expect(pool.disk_path).to match(%r{\A@pools/\h{2}/\h{2}/\h{64}})
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 50920d9d1fc..1c85411dc3b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3898,7 +3898,7 @@ describe Project do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', true do
+ it_behaves_like 'model with uploads', true do
let(:model_object) { create(:project, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
@@ -4092,6 +4092,44 @@ describe Project do
end
end
+ describe '#git_objects_poolable?' do
+ subject { project }
+
+ context 'when the feature flag is turned off' do
+ before do
+ stub_feature_flags(object_pools: false)
+ end
+
+ let(:project) { create(:project, :repository, :public) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when the feature flag is enabled' do
+ context 'when not using hashed storage' do
+ let(:project) { create(:project, :legacy_storage, :public, :repository) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when the project is not public' do
+ let(:project) { create(:project, :private) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when objects are poolable' do
+ let(:project) { create(:project, :repository, :public) }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/uploads/fog_spec.rb b/spec/models/uploads/fog_spec.rb
new file mode 100644
index 00000000000..4a44cf5ab0f
--- /dev/null
+++ b/spec/models/uploads/fog_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Uploads::Fog do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ context 'when object storage is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_uploads_object_storage(FileUploader, enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'model with uploads' do
+ let(:project) { create(:project) }
+ let(:relation) { project.uploads }
+
+ describe '#keys' do
+ let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: project) }
+ subject { data_store.keys(relation) }
+
+ it 'returns keys' do
+ is_expected.to match_array(relation.pluck(:path))
+ end
+ end
+
+ describe '#delete_keys' do
+ let(:keys) { data_store.keys(relation) }
+ let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
+ subject { data_store.delete_keys(keys) }
+
+ before do
+ uploads.each { |upload| upload.build_uploader.migrate!(2) }
+ end
+
+ it 'deletes multiple data' do
+ paths = relation.pluck(:path)
+
+ ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ paths.each do |path|
+ expect(connection.get_object('uploads', path)[:body]).not_to be_nil
+ end
+ end
+
+ subject
+
+ ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ paths.each do |path|
+ expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/uploads/local_spec.rb b/spec/models/uploads/local_spec.rb
new file mode 100644
index 00000000000..3468399f370
--- /dev/null
+++ b/spec/models/uploads/local_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Uploads::Local do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ context 'model with uploads' do
+ let(:project) { create(:project) }
+ let(:relation) { project.uploads }
+
+ describe '#keys' do
+ let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: project) }
+ subject { data_store.keys(relation) }
+
+ it 'returns keys' do
+ is_expected.to match_array(relation.map(&:absolute_path))
+ end
+ end
+
+ describe '#delete_keys' do
+ let(:keys) { data_store.keys(relation) }
+ let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
+ subject { data_store.delete_keys(keys) }
+
+ it 'deletes multiple data' do
+ paths = relation.map(&:absolute_path)
+
+ paths.each do |path|
+ expect(File.exist?(path)).to be_truthy
+ end
+
+ subject
+
+ paths.each do |path|
+ expect(File.exist?(path)).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6cb27246f06..ff075e65c76 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -3231,7 +3231,7 @@ describe User do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', false do
+ it_behaves_like 'model with uploads', false do
let(:model_object) { create(:user, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 7b0192fa9c8..456de5f1b9a 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -165,32 +165,32 @@ describe ProjectPresenter do
describe '#files_anchor_data' do
it 'returns files data' do
- expect(presenter.files_anchor_data).to have_attributes(enabled: true,
- label: 'Files (0 Bytes)',
+ expect(presenter.files_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0 Bytes'),
link: nil)
end
end
describe '#commits_anchor_data' do
it 'returns commits data' do
- expect(presenter.commits_anchor_data).to have_attributes(enabled: true,
- label: 'Commits (0)',
+ expect(presenter.commits_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
describe '#branches_anchor_data' do
it 'returns branches data' do
- expect(presenter.branches_anchor_data).to have_attributes(enabled: true,
- label: "Branches (0)",
+ expect(presenter.branches_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
describe '#tags_anchor_data' do
it 'returns tags data' do
- expect(presenter.tags_anchor_data).to have_attributes(enabled: true,
- label: "Tags (0)",
+ expect(presenter.tags_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
@@ -202,32 +202,32 @@ describe ProjectPresenter do
describe '#files_anchor_data' do
it 'returns files data' do
- expect(presenter.files_anchor_data).to have_attributes(enabled: true,
- label: 'Files (0 Bytes)',
+ expect(presenter.files_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0 Bytes'),
link: presenter.project_tree_path(project))
end
end
describe '#commits_anchor_data' do
it 'returns commits data' do
- expect(presenter.commits_anchor_data).to have_attributes(enabled: true,
- label: 'Commits (0)',
+ expect(presenter.commits_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: presenter.project_commits_path(project, project.repository.root_ref))
end
end
describe '#branches_anchor_data' do
it 'returns branches data' do
- expect(presenter.branches_anchor_data).to have_attributes(enabled: true,
- label: "Branches (#{project.repository.branches.size})",
+ expect(presenter.branches_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including("#{project.repository.branches.size}"),
link: presenter.project_branches_path(project))
end
end
describe '#tags_anchor_data' do
it 'returns tags data' do
- expect(presenter.tags_anchor_data).to have_attributes(enabled: true,
- label: "Tags (#{project.repository.tags.size})",
+ expect(presenter.tags_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including("#{project.repository.tags.size}"),
link: presenter.project_tags_path(project))
end
end
@@ -236,8 +236,8 @@ describe ProjectPresenter do
it 'returns new file data if user can push' do
project.add_developer(user)
- expect(presenter.new_file_anchor_data).to have_attributes(enabled: false,
- label: "New file",
+ expect(presenter.new_file_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including("New file"),
link: presenter.project_new_blob_path(project, 'master'),
class_modifier: 'success')
end
@@ -264,8 +264,8 @@ describe ProjectPresenter do
project.add_developer(user)
allow(project.repository).to receive(:readme).and_return(nil)
- expect(presenter.readme_anchor_data).to have_attributes(enabled: false,
- label: 'Add Readme',
+ expect(presenter.readme_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add README'),
link: presenter.add_readme_path)
end
end
@@ -274,21 +274,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
- expect(presenter.readme_anchor_data).to have_attributes(enabled: true,
- label: 'Readme',
+ expect(presenter.readme_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('README'),
link: presenter.readme_path)
end
end
end
describe '#changelog_anchor_data' do
- context 'when user can push and CHANGELOG does not exists' do
+ context 'when user can push and CHANGELOG does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:changelog).and_return(nil)
- expect(presenter.changelog_anchor_data).to have_attributes(enabled: false,
- label: 'Add Changelog',
+ expect(presenter.changelog_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add CHANGELOG'),
link: presenter.add_changelog_path)
end
end
@@ -297,21 +297,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:changelog).and_return(double(name: 'foo'))
- expect(presenter.changelog_anchor_data).to have_attributes(enabled: true,
- label: 'Changelog',
+ expect(presenter.changelog_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('CHANGELOG'),
link: presenter.changelog_path)
end
end
end
describe '#license_anchor_data' do
- context 'when user can push and LICENSE does not exists' do
+ context 'when user can push and LICENSE does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:license_blob).and_return(nil)
- expect(presenter.license_anchor_data).to have_attributes(enabled: false,
- label: 'Add license',
+ expect(presenter.license_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('Add license'),
link: presenter.add_license_path)
end
end
@@ -320,21 +320,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo'))
- expect(presenter.license_anchor_data).to have_attributes(enabled: true,
- label: presenter.license_short_name,
+ expect(presenter.license_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including(presenter.license_short_name),
link: presenter.license_path)
end
end
end
describe '#contribution_guide_anchor_data' do
- context 'when user can push and CONTRIBUTING does not exists' do
+ context 'when user can push and CONTRIBUTING does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:contribution_guide).and_return(nil)
- expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: false,
- label: 'Add Contribution guide',
+ expect(presenter.contribution_guide_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add CONTRIBUTING'),
link: presenter.add_contribution_guide_path)
end
end
@@ -343,8 +343,8 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo'))
- expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: true,
- label: 'Contribution guide',
+ expect(presenter.contribution_guide_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('CONTRIBUTING'),
link: presenter.contribution_guide_path)
end
end
@@ -355,20 +355,20 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project).to receive(:auto_devops_enabled?).and_return(true)
- expect(presenter.autodevops_anchor_data).to have_attributes(enabled: true,
- label: 'Auto DevOps enabled',
+ expect(presenter.autodevops_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Auto DevOps enabled'),
link: nil)
end
end
- context 'when user can admin pipeline and CI yml does not exists' do
+ context 'when user can admin pipeline and CI yml does not exist' do
it 'returns anchor data' do
project.add_maintainer(user)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil)
- expect(presenter.autodevops_anchor_data).to have_attributes(enabled: false,
- label: 'Enable Auto DevOps',
+ expect(presenter.autodevops_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Enable Auto DevOps'),
link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -380,8 +380,8 @@ describe ProjectPresenter do
project.add_maintainer(user)
cluster = create(:cluster, projects: [project])
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true,
- label: 'Kubernetes configured',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Kubernetes configured'),
link: presenter.project_cluster_path(project, cluster))
end
@@ -390,16 +390,16 @@ describe ProjectPresenter do
create(:cluster, :production_environment, projects: [project])
create(:cluster, projects: [project])
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true,
- label: 'Kubernetes configured',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Kubernetes configured'),
link: presenter.project_clusters_path(project))
end
it 'returns link to create a cluster if no cluster exists' do
project.add_maintainer(user)
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: false,
- label: 'Add Kubernetes cluster',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add Kubernetes cluster'),
link: presenter.new_project_cluster_path(project))
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 2c40e266f5f..f7916441313 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -5,7 +5,6 @@ require_relative '../../../config/initializers/sentry'
describe API::Helpers do
include API::APIGuard::HelperMethods
include described_class
- include SentryHelper
include TermsHelper
let(:user) { create(:user) }
@@ -224,8 +223,15 @@ describe API::Helpers do
describe '.handle_api_exception' do
before do
- allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
allow_any_instance_of(self.class).to receive(:rack_response)
+ allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
+
+ stub_application_setting(
+ sentry_enabled: true,
+ sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
+ )
+ configure_sentry
+ Raven.client.configuration.encoding = 'json'
end
it 'does not report a MethodNotAllowed exception to Sentry' do
@@ -241,10 +247,13 @@ describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
- expect_any_instance_of(self.class).to receive(:sentry_context)
- expect(Raven).to receive(:capture_exception).with(exception, extra: {})
+ expect(Raven).to receive(:capture_exception).with(exception, tags: {
+ correlation_id: 'new-correlation-id'
+ }, extra: {})
- handle_api_exception(exception)
+ Gitlab::CorrelationId.use_id('new-correlation-id') do
+ handle_api_exception(exception)
+ end
end
context 'with a personal access token given' do
@@ -255,7 +264,6 @@ describe API::Helpers do
# We need to stub at a lower level than #sentry_enabled? otherwise
# Sentry is not enabled when the request below is made, and the test
# would pass even without the fix
- expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
get api('/projects', personal_access_token: token)
@@ -272,17 +280,7 @@ describe API::Helpers do
# Sentry events are an array of the form [auth_header, data, options]
let(:event_data) { Raven.client.transport.events.first[1] }
- before do
- stub_application_setting(
- sentry_enabled: true,
- sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
- )
- configure_sentry
- Raven.client.configuration.encoding = 'json'
- end
-
it 'sends the params, excluding confidential values' do
- expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
get api('/projects', user), password: 'dont_send_this', other_param: 'send_this'
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 7497b8f27bd..073c13c2cbb 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -13,39 +13,6 @@ describe DiffFileEntity do
subject { entity.as_json }
- shared_examples 'diff file entity' do
- it 'exposes correct attributes' do
- expect(subject).to include(
- :submodule, :submodule_link, :submodule_tree_url, :file_path,
- :deleted_file, :old_path, :new_path, :mode_changed,
- :a_mode, :b_mode, :text, :old_path_html,
- :new_path_html, :highlighted_diff_lines, :parallel_diff_lines,
- :blob, :file_hash, :added_lines, :removed_lines, :diff_refs, :content_sha,
- :stored_externally, :external_storage, :too_large, :collapsed, :new_file,
- :context_lines_path
- )
- end
-
- it 'includes viewer' do
- expect(subject[:viewer].with_indifferent_access)
- .to match_schema('entities/diff_viewer')
- end
-
- # Converted diff files from GitHub import does not contain blob file
- # and content sha.
- context 'when diff file does not have a blob and content sha' do
- it 'exposes some attributes as nil' do
- allow(diff_file).to receive(:content_sha).and_return(nil)
- allow(diff_file).to receive(:blob).and_return(nil)
-
- expect(subject[:context_lines_path]).to be_nil
- expect(subject[:view_path]).to be_nil
- expect(subject[:highlighted_diff_lines]).to be_nil
- expect(subject[:can_modify_blob]).to be_nil
- end
- end
- end
-
context 'when there is no merge request' do
it_behaves_like 'diff file entity'
end
diff --git a/spec/serializers/discussion_diff_file_entity_spec.rb b/spec/serializers/discussion_diff_file_entity_spec.rb
new file mode 100644
index 00000000000..101ac918a98
--- /dev/null
+++ b/spec/serializers/discussion_diff_file_entity_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DiscussionDiffFileEntity do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diff_refs) { commit.diff_refs }
+ let(:diff) { commit.raw_diffs.first }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
+ let(:entity) { described_class.new(diff_file, request: {}) }
+
+ subject { entity.as_json }
+
+ context 'when there is no merge request' do
+ it_behaves_like 'diff file discussion entity'
+ end
+
+ context 'when there is a merge request' do
+ let(:user) { create(:user) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
+
+ it_behaves_like 'diff file discussion entity'
+
+ it 'exposes additional attributes' do
+ expect(subject).to include(:edit_path)
+ end
+
+ it 'exposes no diff lines' do
+ expect(subject).not_to include(:highlighted_diff_lines,
+ :parallel_diff_lines)
+ end
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 0590304e832..138749b0fdf 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -74,13 +74,5 @@ describe DiscussionEntity do
:active
)
end
-
- context 'when diff file is a image' do
- it 'exposes image attributes' do
- allow(discussion).to receive(:on_image?).and_return(true)
-
- expect(subject.keys).to include(:image_diff_html)
- end
- end
end
end
diff --git a/spec/serializers/trigger_variable_entity_spec.rb b/spec/serializers/trigger_variable_entity_spec.rb
new file mode 100644
index 00000000000..66567c05f52
--- /dev/null
+++ b/spec/serializers/trigger_variable_entity_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe TriggerVariableEntity do
+ let(:project) { create(:project) }
+ let(:request) { double('request') }
+ let(:user) { create(:user) }
+ let(:variable) { { key: 'TEST_KEY', value: 'TEST_VALUE' } }
+
+ subject { described_class.new(variable, request: request).as_json }
+
+ before do
+ allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:project).and_return(project)
+ end
+
+ it 'exposes the variable key' do
+ expect(subject).to include(:key)
+ end
+
+ context 'when user has access to the value' do
+ context 'when user is maintainer' do
+ before do
+ project.team.add_maintainer(user)
+ end
+
+ it 'exposes the variable value' do
+ expect(subject).to include(:value)
+ end
+ end
+
+ context 'when user is owner' do
+ let(:user) { project.owner }
+
+ it 'exposes the variable value' do
+ expect(subject).to include(:value)
+ end
+ end
+ end
+
+ context 'when user does not have access to the value' do
+ before do
+ project.team.add_developer(user)
+ end
+
+ it 'does not expose the variable value' do
+ expect(subject).not_to include(:value)
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index e779675744c..87185891470 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -20,9 +20,9 @@ describe Ci::RetryBuildService do
CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
REJECT_ACCESSORS =
- %i[id status user token coverage trace runner artifacts_expire_at
- artifacts_file artifacts_metadata artifacts_size created_at
- updated_at started_at finished_at queued_at erased_by
+ %i[id status user token token_encrypted coverage trace runner
+ artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
+ created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 0bd7719345e..1a2ca23748a 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -31,6 +31,31 @@ describe Clusters::Applications::CreateService do
subject
end
+ context 'cert manager application' do
+ let(:params) do
+ {
+ application: 'cert_manager',
+ email: 'test@example.com'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_cert_manager)
+ end
+
+ it 'sets the email' do
+ expect(subject.email).to eq('test@example.com')
+ end
+ end
+
context 'jupyter application' do
let(:params) do
{
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d29a1091d95..1d9c75dedce 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -621,4 +621,77 @@ describe MergeRequests::RefreshService do
@fork_build_failed_todo.reload
end
end
+
+ describe 'updating merge_commit' do
+ let(:service) { described_class.new(project, user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:oldrev) { TestEnv::BRANCH_SHA['merge-commit-analyze-before'] }
+ let(:newrev) { TestEnv::BRANCH_SHA['merge-commit-analyze-after'] } # Pretend branch is now updated
+
+ let!(:merge_request) do
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'merge-commit-analyze-after',
+ target_branch: 'merge-commit-analyze-before',
+ target_project: project,
+ merge_user: user
+ )
+ end
+
+ let!(:merge_request_side_branch) do
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'merge-commit-analyze-side-branch',
+ target_branch: 'merge-commit-analyze-before',
+ target_project: project,
+ merge_user: user
+ )
+ end
+
+ subject { service.execute(oldrev, newrev, 'refs/heads/merge-commit-analyze-before') }
+
+ context 'feature enabled' do
+ before do
+ stub_feature_flags(branch_push_merge_commit_analyze: true)
+ end
+
+ it "updates merge requests' merge_commits" do
+ expect(Gitlab::BranchPushMergeCommitAnalyzer).to receive(:new).and_wrap_original do |original_method, commits|
+ expect(commits.map(&:id)).to eq(%w{646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9})
+
+ original_method.call(commits)
+ end
+
+ subject
+
+ merge_request.reload
+ merge_request_side_branch.reload
+
+ expect(merge_request.merge_commit.id).to eq('646ece5cfed840eca0a4feb21bcd6a81bb19bda3')
+ expect(merge_request_side_branch.merge_commit.id).to eq('29284d9bcc350bcae005872d0be6edd016e2efb5')
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(branch_push_merge_commit_analyze: false)
+ end
+
+ it "does not trigger analysis" do
+ expect(Gitlab::BranchPushMergeCommitAnalyzer).not_to receive(:new)
+
+ subject
+
+ merge_request.reload
+ merge_request_side_branch.reload
+
+ expect(merge_request.merge_commit).to eq(nil)
+ expect(merge_request_side_branch.merge_commit).to eq(nil)
+ end
+ end
+ end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index a3d24ae312a..26e8d829345 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe Projects::ForkService do
include ProjectForksHelper
- let(:gitlab_shell) { Gitlab::Shell.new }
+ include Gitlab::ShellAdapter
+
context 'when forking a new project' do
describe 'fork by user' do
before do
@@ -235,6 +236,33 @@ describe Projects::ForkService do
end
end
+ context 'when forking with object pools' do
+ let(:fork_from_project) { create(:project, :public) }
+ let(:forker) { create(:user) }
+
+ before do
+ stub_feature_flags(object_pools: true)
+ end
+
+ context 'when no pool exists' do
+ it 'creates a new object pool' do
+ forked_project = fork_project(fork_from_project, forker)
+
+ expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
+ end
+ end
+
+ context 'when a pool already exists' do
+ let!(:pool_repository) { create(:pool_repository, source_project: fork_from_project) }
+
+ it 'joins the object pool' do
+ forked_project = fork_project(fork_from_project, forker)
+
+ expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
+ end
+ end
+ end
+
context 'when linking fork to an existing project' do
let(:fork_from_project) { create(:project, :public) }
let(:fork_to_project) { create(:project, :public) }
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 1f00cdf7e92..d52c40ff4f1 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -54,6 +54,9 @@ module TestEnv
'add_images_and_changes' => '010d106',
'update-gitlab-shell-v-6-0-1' => '2f61d70',
'update-gitlab-shell-v-6-0-3' => 'de78448',
+ 'merge-commit-analyze-before' => '1adbdef',
+ 'merge-commit-analyze-side-branch' => '8a99451',
+ 'merge-commit-analyze-after' => '646ece5',
'2-mb-file' => 'bf12d25',
'before-create-delete-modify-move' => '845009f',
'between-create-delete-modify-move' => '3f5f443',
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index 47ad0c6345d..1d11b855459 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
+shared_examples_for 'model with uploads' do |supports_fileuploads|
describe '.destroy' do
before do
stub_uploads_object_storage(uploader_class)
@@ -8,16 +8,62 @@ shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE)
end
- it 'deletes remote uploads' do
- expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
+ context 'with mounted uploader' do
+ it 'deletes remote uploads' do
+ expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
- expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ end
end
- it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do
- create(:upload, uploader: FileUploader, model: model_object)
+ context 'with not mounted uploads', :sidekiq, skip: !supports_fileuploads do
+ context 'with local files' do
+ let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: model_object) }
- expect { model_object.destroy }.to change { Upload.count }.by(-2)
+ it 'deletes any FileUploader uploads which are not mounted' do
+ expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ end
+
+ it 'deletes local files' do
+ expect_any_instance_of(Uploads::Local).to receive(:delete_keys).with(uploads.map(&:absolute_path))
+
+ model_object.destroy
+ end
+ end
+
+ context 'with remote files' do
+ let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: model_object) }
+
+ it 'deletes any FileUploader uploads which are not mounted' do
+ expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ end
+
+ it 'deletes remote files' do
+ expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(uploads.map(&:path))
+
+ model_object.destroy
+ end
+ end
+
+ describe 'destroy strategy depending on feature flag' do
+ let!(:upload) { create(:upload, uploader: FileUploader, model: model_object) }
+
+ it 'does not destroy uploads by default' do
+ expect(model_object).to receive(:delete_uploads)
+ expect(model_object).not_to receive(:destroy_uploads)
+
+ model_object.destroy
+ end
+
+ it 'uses before destroy callback if feature flag is disabled' do
+ stub_feature_flags(fast_destroy_uploads: false)
+
+ expect(model_object).to receive(:destroy_uploads)
+ expect(model_object).not_to receive(:delete_uploads)
+
+ model_object.destroy
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
new file mode 100644
index 00000000000..b8065886c42
--- /dev/null
+++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+shared_examples 'diff file base entity' do
+ it 'exposes essential attributes' do
+ expect(subject).to include(:content_sha, :submodule, :submodule_link,
+ :submodule_tree_url, :old_path_html,
+ :new_path_html, :blob, :can_modify_blob,
+ :file_hash, :file_path, :old_path, :new_path,
+ :collapsed, :text, :diff_refs, :stored_externally,
+ :external_storage, :renamed_file, :deleted_file,
+ :mode_changed, :a_mode, :b_mode, :new_file)
+ end
+
+ # Converted diff files from GitHub import does not contain blob file
+ # and content sha.
+ context 'when diff file does not have a blob and content sha' do
+ it 'exposes some attributes as nil' do
+ allow(diff_file).to receive(:content_sha).and_return(nil)
+ allow(diff_file).to receive(:blob).and_return(nil)
+
+ expect(subject[:context_lines_path]).to be_nil
+ expect(subject[:view_path]).to be_nil
+ expect(subject[:highlighted_diff_lines]).to be_nil
+ expect(subject[:can_modify_blob]).to be_nil
+ end
+ end
+end
+
+shared_examples 'diff file entity' do
+ it_behaves_like 'diff file base entity'
+
+ it 'exposes correct attributes' do
+ expect(subject).to include(:too_large, :added_lines, :removed_lines,
+ :context_lines_path, :highlighted_diff_lines,
+ :parallel_diff_lines)
+ end
+
+ it 'includes viewer' do
+ expect(subject[:viewer].with_indifferent_access)
+ .to match_schema('entities/diff_viewer')
+ end
+end
+
+shared_examples 'diff file discussion entity' do
+ it_behaves_like 'diff file base entity'
+end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index fc1fe5739c3..006c93686d5 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -23,7 +23,7 @@ describe 'projects/_home_panel' do
it 'makes it possible to set notification level' do
render
- expect(view).to render_template('shared/notifications/_button')
+ expect(view).to render_template('projects/buttons/_notifications')
expect(rendered).to have_selector('.notification-dropdown')
end
end
diff --git a/spec/workers/object_pool/create_worker_spec.rb b/spec/workers/object_pool/create_worker_spec.rb
new file mode 100644
index 00000000000..06416489472
--- /dev/null
+++ b/spec/workers/object_pool/create_worker_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ObjectPool::CreateWorker do
+ let(:pool) { create(:pool_repository, :scheduled) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'when the pool creation is successful' do
+ it 'marks the pool as ready' do
+ subject.perform(pool.id)
+
+ expect(pool.reload).to be_ready
+ end
+ end
+
+ context 'when a the pool already exists' do
+ before do
+ pool.create_object_pool
+ end
+
+ it 'cleans up the pool' do
+ expect do
+ subject.perform(pool.id)
+ end.to raise_error(GRPC::FailedPrecondition)
+
+ expect(pool.reload.failed?).to be(true)
+ end
+ end
+
+ context 'when the server raises an unknown error' do
+ before do
+ allow_any_instance_of(PoolRepository).to receive(:create_object_pool).and_raise(GRPC::Internal)
+ end
+
+ it 'marks the pool as failed' do
+ expect do
+ subject.perform(pool.id)
+ end.to raise_error(GRPC::Internal)
+
+ expect(pool.reload.failed?).to be(true)
+ end
+ end
+
+ context 'when the pool creation failed before' do
+ let(:pool) { create(:pool_repository, :failed) }
+
+ it 'deletes the pool first' do
+ expect_any_instance_of(PoolRepository).to receive(:delete_object_pool)
+
+ subject.perform(pool.id)
+
+ expect(pool.reload).to be_ready
+ end
+ end
+ end
+end
diff --git a/spec/workers/object_pool/join_worker_spec.rb b/spec/workers/object_pool/join_worker_spec.rb
new file mode 100644
index 00000000000..906bc22c8d2
--- /dev/null
+++ b/spec/workers/object_pool/join_worker_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ObjectPool::JoinWorker do
+ let(:pool) { create(:pool_repository, :ready) }
+ let(:project) { pool.source_project }
+ let(:repository) { project.repository }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context "when the pool is not joinable" do
+ let(:pool) { create(:pool_repository, :scheduled) }
+
+ it "doesn't raise an error" do
+ expect do
+ subject.perform(pool.id, project.id)
+ end.not_to raise_error
+ end
+ end
+
+ context 'when the pool has been joined before' do
+ before do
+ pool.link_repository(repository)
+ end
+
+ it 'succeeds in joining' do
+ expect do
+ subject.perform(pool.id, project.id)
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
index d7d64a1f641..b3ec71d4a00 100644
--- a/spec/workers/prune_web_hook_logs_worker_spec.rb
+++ b/spec/workers/prune_web_hook_logs_worker_spec.rb
@@ -5,18 +5,20 @@ describe PruneWebHookLogsWorker do
before do
hook = create(:project_hook)
- 5.times do
- create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
- end
-
+ create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 4.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 91.days.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 89.days.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 2.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 1.month.ago)
create(:web_hook_log, web_hook: hook, response_status: '404')
end
- it 'removes all web hook logs older than one month' do
+ it 'removes all web hook logs older than 90 days' do
described_class.new.perform
- expect(WebHookLog.count).to eq(1)
- expect(WebHookLog.first.response_status).to eq('404')
+ expect(WebHookLog.count).to eq(4)
+ expect(WebHookLog.last.response_status).to eq('404')
end
end
end
diff --git a/spec/workers/remove_old_web_hook_logs_worker_spec.rb b/spec/workers/remove_old_web_hook_logs_worker_spec.rb
deleted file mode 100644
index 6d26ba5dfa0..00000000000
--- a/spec/workers/remove_old_web_hook_logs_worker_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe RemoveOldWebHookLogsWorker do
- subject { described_class.new }
-
- describe '#perform' do
- let!(:week_old_record) { create(:web_hook_log, created_at: Time.now - 1.week) }
- let!(:three_days_old_record) { create(:web_hook_log, created_at: Time.now - 3.days) }
- let!(:one_day_old_record) { create(:web_hook_log, created_at: Time.now - 1.day) }
-
- it 'removes web hook logs older than 2 days' do
- subject.perform
-
- expect(WebHookLog.all).to include(one_day_old_record)
- expect(WebHookLog.all).not_to include(week_old_record, three_days_old_record)
- end
- end
-end